Avoiding stale state in React useEffect

Avoiding stale state in React useEffect

Sometimes it's easy for me to forget I'm using JavaScript when building React apps.

I've been using Lit for a long time. In Lit, it's plainly clear that I'm using JavaScript and working with the DOM. In React, that gets abstracted away. But it doesn't change the fact that the rules of JavaScript still apply.

Why is my state getting reset on every useEffect call in React?

I'm building a chat application for an upcoming Hilla presentation. I want to subscribe to incoming messages through a web socket, and I only want to set up the connection once when the component gets mounted.

This was my initial attempt:

export function ChatView() {
  const [messages, setMessages] = useState<Message[]>([]);

  useEffect(() => {
    const subscription = ChatService
      .join()
      .onNext(message => 
        setMessages([...messages, message]));

    return () => {
      subscription.cancel();
    };
  }, []);

  return (
    // view code
  )
}

Looks straightforward enough: subscribe to the service, update the messages array on incoming messages, and close the subscription on unmount. Only run the effect once by passing in an empty deps array [].

The problem? My message list only contained the latest message instead of the full chat history.

After a few minutes of debugging, it dawned on me: because the effect only runs once, the value of messages in the closure is still the value it had when the component was created.

Updating state from a useEffect hook

The solution was simple. To update a state that's dependent on the previous value of the state, you need to do a functional update by passing in a function to setState. The function gets the previous state as a parameter, which avoids the stale state issue.

export function ChatView() {
  const [messages, setMessages] = useState<Message[]>([]);

  useEffect(() => {
    const subscription = ChatService
      .join()
      .onNext(message =>
        setMessages(prev => [...prev, message]));

    return () => {
      subscription.cancel();
    };
  }, []);

  return (
    // view code
  )
}

That's it!

You can find the full code for my demo on my GitHub https://github.com/marcushellberg/hilla-react-chat