Part V β The Craft Β· Lesson 10.13
Real-Time Updates
In one lineMerge live events into rendered lists without losing the user's place or their scroll position.
Where you've seen itA live order screen updating itself, and a feed that politely says "3 new posts" instead of jumping.
Diagram
THE PIPELINE
transport (2.4 / 2.5)
β {type:'post.updated', id:'p7', data:{β¦}, seq: 812}
βΌ
ββββββββββββββββ dedupe by seq/id Β· ignore stale (seq < last)
β NORMALISE β reshape to your entity form (6.6)
ββββββββ¬ββββββββ
βΌ
ββββββββββββββββ update β merge into byId
β REDUCER β create β add to byId, decide list placement
ββββββββ¬ββββββββ delete β tombstone, then remove
βΌ
ββββββββββββββββ only components selecting that entity
β UI β re-render (10.5)
ββββββββββββββββINSERTS: NEVER YANK THE VIEWPORT
user at top of list user scrolled down
ββββββββββββββββββ ββββββββββββββββββ
β [3 new posts β²]β β banner β item 40 β
β item 1 β β item 41 β β new items appended
β item 2 β β item 42 β ABOVE would push
ββββββββββββββββββ ββββββββββββββββββ content down
insert on click buffer them; keep scroll anchored
Chat is the exception: pin to bottom ONLY if already at bottom.LOCAL EDIT vs SERVER EVENT β the conflict everyone hits
user typing in the row server event arrives for that row
β β
ββββββββββββββββ¬ββββββββββββββββ
βΌ
is the field dirty (locally edited)?
βββββββββββββββ΄ββββββββββββββ
yes no
β β
keep local value apply server value
+ mark "updated elsewhere"How it works
- Give every event a sequence number or version, and drop anything older than what you've applied β out-of-order delivery is normal.
- Reduce into a normalized store (6.6) so one event updates every view showing that entity.
- Buffer inserts behind a "N new items" affordance unless the user is at the top; never shift content under a reading user.
- Protect local edits. Track dirty fields and don't let a server event overwrite something the user is typing.
- Reconcile on reconnect: after any gap, fetch the delta since your last sequence number rather than trusting the stream (2.4).
function reducer(state, event) {
if (event.seq <= state.lastSeq) return state; // stale
switch (event.type) {
case 'post.updated':
return { ...state, lastSeq: event.seq,
byId: { ...state.byId, [event.id]: merge(state.byId[event.id], event.data) } };
case 'post.created':
return { ...state, lastSeq: event.seq, pending: [event.id, ...state.pending] };
default:
return { ...state, lastSeq: event.seq };
}
}Trade-offs
| Choice | Consequence |
|---|---|
| Apply inserts immediately | Content jumps under the user |
| Buffer with a banner | Slightly stale, always comfortable |
| Full refetch on event | Simple, wasteful, loses scroll |
| Patch by id | Efficient, needs normalization |
Throttle the UI, not the data. At high event rates, batch state updates into one render per animation frame; the network can handle 200 events per second, React re-rendering 200 times cannot.
Interview angle
"Live updates arrive while the user is scrolled into the feed. What do you do?"
- Buffer inserts behind a "N new posts" banner and keep scroll anchored; only auto-insert when the user is at the top.
- Patch entities by id in a normalized store so unrelated rows don't re-render.
- Handle sequence numbers, reconnect deltas, and dirty local fields β those three are what separate a real implementation from a demo.
Recap
- Sequence numbers make out-of-order and duplicate events harmless.
- Buffer inserts; never move content under someone who's reading.
- Batch renders per frame β the UI is the bottleneck, not the socket.