I have a SignalR (WebSocket) server for a game, and a React/Redux client that connects to it. I have a component whose task is to listen to SignalR messages to handle them, which usually involves updating something in the Redux state. One of my messages, however, is called "BattleEvents", and this message is actually multiple events packed into one single message. When I receive this, I simply loop through each event in the message and call its corresponding handler function. This is a minimal example of this component:
function BattleEventListener({
roomId
}: BattleEventListenerProps) {
const dispatch = useDispatch();
const roomState = useSelector((state: RootState) => state.rooms.byId[roomId]);
const roomRef = useRef(roomState);
useEffect(() => {
if (gameHub.hub === null) return;
console.info("Connecting room to gameHub.");
gameHub.hub.on('BattleEvents', handleBattleEvents);
return () => {
gameHub.hub?.off('BattleEvents', handleBattleEvents);
}
}, [dispatch, gameHub.hub, roomRef.current]);
useEffect(() => {
roomRef.current = roomState;
}, [roomState]);
return null;
async function handleBattleEvents (evtId: string, roomId: string, events: BattleEvent[]) {
console.debug('<< BattleEvents', { evtId, roomId, events });
for (let evt of events) {
try {
//...
else if (evt.type === 'takeDamage') handleTakeDamage(evtId, roomId, evt);
//...
}
catch (err) {
//...
}
}
}
// ...
function handleTakeDamage (
evtId: string, roomId: string, evt: TakeDamageEvent
) {
const room = roomRef.current;
const fighter = room.battle.players[evt.playerId].fighters[evt.fighterId];
const oldHpPerc = fighter.currentHpPerc; // <-- Here we are reading an outdated value
dispatch(roomsActions.setFighterHpPerc({
roomId,
playerId: evt.playerId,
fighterId: evt.fighterId,
hpPerc: evt.currentHpPerc,
}));
console.log(oldHpPerc) // <-- will always print 1 (the initial value) in the first batch of events.
}
}
The problem in this code is that, when we receive, for example, 3 events in one message, we'll call handleTakeDamage() 3 times, and all of these calls will have the initial room value. Every time I read fighter.currentHpPerc, I'll get one, even if the previous call set that variable to a different value (e.g. 0.73). The next time we receive a "BattleEvents" message and process a new batch of events, we'll read the last value set by the previous handlers (e.g. if the last call set it to 0.73, now all the handleTakeDamage() we handle ini this batch will read 0.73).
I suspect this occurs because the value in roomState received by useSelector will not be updated until all 3 calls to handleTakeDamage() are completed. Adding await Promise.resolve() to every loop of the for loop (which makes every loop take one js 'frame') solves this problem, which seems to confirm my suspicion. However, that is a hack and I'm sure there's a correct way to solve this problem, as my use case shouldn't be uncommon.