如何使用 socket.io 处理同步的 React 状态更新

How to handle simultaneous React state updates with socket.io

给定以下 React 功能组件:

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;

每次通过 socket.io websocket 连接收到 event1event2 消息时,我想更新 events 状态变量。如果消息以合理的间隔传入,则此方法工作正常,但当 event1event2,或多个 event1event2 消息非常快地传入时,问题就会出现。在处理下一个事件之前状态更新没有足够的时间完成,导致 events 数组被最后一次调用 setEvents 覆盖。有没有办法确保每条新消息都添加到 events 状态对象?我需要手动批量状态更新吗?我可以使用另一个挂钩来保存更新之间的状态吗?

pushEvent 函数是 useEffect 的依赖项,但不在其依赖项数组中。这意味着更新使用指向原始空 events 数组的初始函数。

您可以将其添加为 useEffect() 的依赖项:

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

但是,这会导致在每次渲染时调用 useEffect,因为 pushEvent 是在渲染时重新创建的,因此您应该避免这种情况。 相反,使用 functional updates 设置状态以使新状态依赖于前一个状态:

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

问题

我看到的问题是,如果多个 event1 或 event2 事件碰巧在单个渲染周期内将状态更新排入队列,那么每个处理过的更新都会停止先前的更新。实际上,最后处理的状态更新是在后续渲染周期中看到的。

解决方案

使用 functional state 更新从以前的状态正确更新,而不是使用当前 callback/render 范围内可能关闭的任何内容。您还可以将 pushEvent 处理程序 移动到 useEffect 回调中,以将其作为外部依赖项删除。

示例:

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);
  });
}, []);