0

Ref: https://stackblitz.com/edit/react-ts-8ykcuf?file=index.tsx

I created a small example to replicate the issue I am facing.

I am trying to create a delayed effect with setTimeout inside useEffect. I can see from console.log that setTimeout has already triggered and I expect the DOM to be updated, but actually the DOM is not rendered until the next human interaction.

The side effect in the sample example is to simulate a bot appending new message after user has entered a new message.

import React, { useEffect, useState } from 'react';
import { render } from 'react-dom';

interface Chat {
  messages: string[];
  poster: string;
}

const App = () => {
  const [activeChat, setActiveChat] = useState<Chat>({
    poster: 'Adam',
    messages: ['one', 'two', 'three'],
  });

  const [message, setMessage] = useState('');
  const [isBotChat, setIsBotChat] = useState(false);

  useEffect(() => {
    if (isBotChat) {
      setIsBotChat(false);
      setTimeout(() => {
        activeChat.messages.push('dsadsadsada');
        console.log('setTimeout completed');
      }, 500);
    }
  }, [isBotChat]);

  const handleSubmit = (e) => {
    e.preventDefault();

    if (message !== '') {
      activeChat.messages.push(message);
      setMessage('');
      setIsBotChat(true);
    }
  };

  return (
    <div>
      <h1>Active Chat</h1>
      <div>
        {activeChat?.messages.map((m, index) => (
          <div key={index}>{m}</div>
        ))}
      </div>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.currentTarget.value)}
        />
        <button type="submit">Send Message</button>
      </form>
    </div>
  );
};

render(<App />, document.getElementById('root'));
2
  • 1
    You are directly mutating the state with activeChat.messages.push(), you need to set state using Reacts methods (setActiveChat) otherwise it will not know about the change. Commented Nov 12, 2021 at 16:38
  • I have used setActiveChat and many other methods, they all have same issue. For example activeChat.messages.push('dsadsadsada'); setActiveChat(activeChat); will still not trigger rendering. Can you please show exactly how to structure it? Also, in this case, I am not changing activeChat. I am changing its properties only. Why do I need to use setActiveChat? If I use setActiveChat, then many instance references will be broken. Commented Nov 12, 2021 at 16:45

1 Answer 1

2

To set your state you need to use setActiveChat, in this case something like:

setActiveChat(previous => ({
  ...previous,
  messages: [...previous.messages, 'dsadsadsada']
}))

The set state function React provides can accept a function, which we'll use in this case to avoid race conditions. previous is the previous value of activeChat (We can't rely on activeChat itself being up to date yet since the current render may be out of sync with the state) Then we expand the existing state and add the new property.

In your comments you mention only changing properties, and I'm afraid it's really not recommended to change anything in state directly, there are several in depth explanations of that here (StackOverflow question), and here (Documentation).

Full example (StackBlitz):

import React, { useEffect, useState } from 'react';
import { render } from 'react-dom';
import './style.css';

interface Chat {
  messages: string[];
  poster: string;
}

const App = () => {
  const [activeChat, setActiveChat] = useState<Chat>({
    poster: 'Adam',
    messages: ['one', 'two', 'three'],
  });

  const [message, setMessage] = useState('');
  const [isBotChat, setIsBotChat] = useState(false);

  useEffect(() => {
    if (isBotChat) {
      setIsBotChat(false);
      setTimeout(() => {
        setActiveChat(previous => ({
          ...previous,
          messages: [...previous.messages, 'dsadsadsada']
        }))
        console.log('setTimeout completed');
      }, 500);
    }
  }, [isBotChat]);

  const handleSubmit = (e) => {
    e.preventDefault();

    if (message !== '') {
      setActiveChat(previous => ({
        ...previous,
        messages: [...previous.messages, message]
      }))
      setMessage('');

      setTimeout(() => {
        setActiveChat(previous => ({
          ...previous,
          messages: [...previous.messages, 'dsadsadsada']
        }))
        console.log('setTimeout completed');
      }, 500);
    }
  };

  return (
    <div>
      <h1>Active Chat</h1>
      <div>
        {activeChat?.messages.map((m, index) => (
          <div key={index}>{m}</div>
        ))}
      </div>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.currentTarget.value)}
        />
        <button type="submit">Send Message</button>
      </form>
    </div>
  );
};

render(<App />, document.getElementById('root'));

Sign up to request clarification or add additional context in comments.

5 Comments

If I use setActiveChat like you shown, I will lose the reference to other parts of the app where the activeChat was used and the other parts will not update. Please see example: stackblitz.com/edit/react-ts-8ykcuf?file=index.tsx ... in that case how do we handle multi-level relationship like this?
Hmm, you would normally only store the chat data in a single state. One possible way you could do this with minimal changes would be to store the active chat's index in activeChat and use that to access chats. Then change the setActiveChat to setChats with the relevant updated data whenever you need to update the content.
Example of the above method here
Hmm... that means in ReactJS, I have to manually update everywhere where the data is used, or split them into their own state to the lowest level of detail that can be shared by different components. I have been trying the activeChatId method as well, but I can't decide whether to use the array index as the id or the database id as the id for referencing. It seems there's a lot of overhead doing the setXXX, array.filter and array.find.
Thanks for your advice; appreciate your help with the code samples. I will experiment more.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.