FSD Frontend System Design

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.

  • Time17 min
  • DiagramEvent β†’ reducer β†’ UI, and the merge conflict with local edits
  • Chapter10 Β· Low Level Design

Diagram

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)
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
diagram
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.
diagram
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

  1. Give every event a sequence number or version, and drop anything older than what you've applied β€” out-of-order delivery is normal.
  2. Reduce into a normalized store (6.6) so one event updates every view showing that entity.
  3. Buffer inserts behind a "N new items" affordance unless the user is at the top; never shift content under a reading user.
  4. Protect local edits. Track dirty fields and don't let a server event overwrite something the user is typing.
  5. Reconcile on reconnect: after any gap, fetch the delta since your last sequence number rather than trusting the stream (2.4).
js
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

ChoiceConsequence
Apply inserts immediatelyContent jumps under the user
Buffer with a bannerSlightly stale, always comfortable
Full refetch on eventSimple, wasteful, loses scroll
Patch by idEfficient, 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.