Stop 12 · Real-time FeaturesTopics · 4/4

Real-time State Sync

Part of the Full Stack Developer Roadmap

In short

A WebSocket event says 'card 9 moved to Done'. Now what? Merging that into your React state without duplicates, flicker or stale overwrites is the part most tutorials skip.

Keeping your frontend's displayed state correctly synchronized with real-time events from the backend — merging live updates into React Query's cache or local state without causing UI glitches.

The blob character raises a hand to bid among crates of tomatoes and onions at a busy mandi.

Think of it like this

Calls at the mandi auction

At a mandi auction, the auctioneer calls out each new bid and everyone updates the price in their head. If you step out for chai, you don't guess when you return. You ask 'what's the price now?' and then follow the calls again. Live events update state; reconnecting means refetching first.

Watch it happen

One move, two tabs, one truth

Asha's tab
Socket server
Postgres
Ravi's tab

card:move {id 9, Done}

1/6Asha drags card 9 to Done. Her UI moves it instantly and emits the change.

The big ideas

What you need to know

Merge, don't append

Update items by id. Pushing every event into an array creates duplicates when the same change arrives twice.

Versions beat timestamps

Send a version number with each change, and ignore events older than what you already have.

Missed events are normal

Connections drop. On reconnect, refetch the current state, then apply new events on top.

The server is the referee

Clients send intents. The server saves, decides the final value and broadcasts it, so every screen ends up the same.

See it in code

Walk through the code

tsx
1useEffect(() => {2  const onMoved = (card: Card) => {3    queryClient.setQueryData<Card[]>(["board", boardId], (old = []) =>4      old.map((c) => (c.id === card.id && card.version > c.version ? card : c)),5    );6  };7  const onReconnect = () => queryClient.invalidateQueries({ queryKey: ["board", boardId] });8 9  socket.on("card:moved", onMoved);10  socket.io.on("reconnect", onReconnect);11  return () => {12    socket.off("card:moved", onMoved);13    socket.io.off("reconnect", onReconnect);14  };15}, [boardId]);

1/3Each event replaces the matching card, but only if its version is newer. Old or repeated events change nothing.

Socket.io client plus TanStack Query. The board's cached list is the single place real-time events land.

Save yourself the pain

Common mistakes

Oops

Appending every incoming event to state with [...old, item].

Do this instead

Upsert by id. The same event can arrive twice, especially around reconnects.

Oops

Forgetting socket.off in the effect cleanup.

Do this instead

Without cleanup, Strict Mode and page changes stack up listeners, and each event gets applied several times.

Oops

Trusting the cache after a reconnect.

Do this instead

Refetch once when the socket reconnects. Events sent while you were offline are gone for good.

Your mission

Two windows, one board

~35 min

Quick check

Ravi's tab receives card 9 with version 11, but his cache already holds version 12. What should it do?

Go deeper

  1. 1Update a React Query cache directly when a real-time event arrives, instead of refetching everything.
  2. 2Handle out-of-order or duplicate real-time events gracefully.
  3. 3Test what happens to your UI when the real-time connection briefly drops and reconnects.

More topics at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge