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