Home > front end >  How to handle simultaneous React state updates with socket.io
How to handle simultaneous React state updates with socket.io

Time:04-29

Given the following React functional component:

const DataLog: FC = (props) => {
  const [events, setEvents] = useState<string[]>([]);

  const pushEvent = (event: string) => {
    const updatedEvents = [...events];
    updatedEvents.push(event);
    setEvents([...updatedEvents]);
  };

  useEffect(() => {
    const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io(
      'http://localhost:9000'
    );

    socket.on('connect', () => {
      console.log('Web socket connected');
    });

    socket.on('event1', (event: string) => {
      pushEvent(event);
    });

    socket.on('event2', (event: string) => {
      pushEvent(event);
    });
  }, []);

  return (
    <div>
      {events.map((event: string, index: number) => {
        return <div key={index}>{event}</div>;
      })}
    </div>
  );
};

export default DataLog;

I would like to update the events state variable each time an event1 or event2 message is received via the socket.io websocket connection. This works fine if the messages are coming in reasonably spaced out, but the issue arises when event1 and event2, or multiple event1 or event2 messages come in very rapidly. The state update doesn't have enough time to complete before the next event is being processed resulting in the events array being overwritten by the last call to setEvents. Is there a way I can ensure each new message gets added to the events state object? Do I need to manually batch state updates? Is there another hook I can use to preserve state between updates?

CodePudding user response:

The pushEvent function is a dependency of the useEffect, but isn't in it's dependencies array. This means that the updates use the initial function which points to the original empty events array.

You can add it as a dependency of useEffect():

useEffect(() => {
  // your code
}, [pushEvent]);

However, this would cause the useEffect to be called on each render, since pushEvent is recreated on render, so you should probably avoid that. Instead, set the state using functional updates to make the new state dependent on the previous one:

const DataLog: FC = (props) => {
  const [events, setEvents] = useState<string[]>([]);

  useEffect(() => {
    const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io(
      'http://localhost:9000'
    );

    socket.on('connect', () => {
      console.log('Web socket connected');
    });

    socket.on('event1', (event: string) => {
      setEvents(prev => [...prev, event]);
    });

    socket.on('event2', (event: string) => {
      setEvents(prev => [...prev, event]);
    });
  }, []);

  return (
    <div>
      {events.map((event: string, index: number) => {
        return <div key={index}>{event}</div>;
      })}
    </div>
  );
};

CodePudding user response:

Issue

The issue I see is that if multiple event1 or event2 events happen to enqueue state updates within a single render cycle then each processed update stomps the previous update. In effect, the last processed state update is the one seen on the subsequent render cycle.

Solution

Use a functional state update to correctly update from the previous state instead of using whatever may be closed over in the current callback/render scope. You can also move the pushEvent handler into the useEffect callback to remove it as an external dependency.

Example:

useEffect(() => {
  const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io(
    'http://localhost:9000'
  );

  socket.on('connect', () => {
    console.log('Web socket connected');
  });

  const pushEvent = (event: string) => {
    setEvents(events => [...events, event]); // functional update
  };

  socket.on('event1', (event: string) => {
    pushEvent(event);
  });

  socket.on('event2', (event: string) => {
    pushEvent(event);
  });
}, []);
  • Related