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'));
Jake
  • 11,273
  • 21
  • 90
  • 147
  • 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. – DBS Nov 12 '21 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. – Jake Nov 12 '21 at 16:45

1 Answers1

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'));

DBS
  • 9,110
  • 4
  • 35
  • 53
  • 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: https://stackblitz.com/edit/react-ts-8ykcuf?file=index.tsx ... in that case how do we handle multi-level relationship like this? – Jake Nov 12 '21 at 17:06
  • 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. – DBS Nov 12 '21 at 17:14
  • Example of the above method [here](https://stackblitz.com/edit/react-ts-pydrdb?file=index.tsx) – DBS Nov 12 '21 at 17:20
  • 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`. – Jake Nov 12 '21 at 17:25
  • Thanks for your advice; appreciate your help with the code samples. I will experiment more. – Jake Nov 12 '21 at 17:26