Skip to main content

Command Palette

Search for a command to run...

Avoiding stale state in React useEffect

Updated
2 min read
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

More from this blog

Marcus Hellberg

8 posts

Marcus is a long-time Java and web developer. He's curious to learn technologies and excited to share his insights with others. He is the VP of DevRel at Vaadin and an international speaker.