FrontendPro

114Debug React 18 concurrent rendering inconsistencies involving mutable external data

hardAbout 60 minutesCodingreactconcurrent-renderingtearinguseSyncExternalStoredebugging

Problem

A live-score widget reads from a small store shared with non-React code (a WebSocket handler calls store.increment()). Users report that some components show a stale score, that the score sometimes never updates after the page loads, and that two components on the same screen occasionally disagree.

function createCounterStore() {
  const state = { count: 0 };
  const listeners = new Set();
  return {
    state,
    increment() {
      state.count++; // mutates in place
      listeners.forEach((listener) => listener());
    },
    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
  };
}

function useCount(store) {
  const [, forceRender] = useReducer((x) => x + 1, 0);
  useEffect(() => store.subscribe(forceRender), [store]); // subscribes after paint
  return store.state.count; // reads mutable data during render
}

Fix both functions. Keep the API used by components (useCount(store), store.increment(), store.subscribe(listener)), and add store.getSnapshot() that returns an immutable snapshot object { count }, replaced on every change.

Your fix must guarantee:

  • an update that happens between render and subscription (for example in another component's layout effect, or a WebSocket message right after mount) is not missed;
  • every component shows the same value after an update;
  • old snapshots are never mutated;
  • the subscription is removed on unmount.

Try it yourself

Write your solution, run it, then submit to check it against the tests.

App.jsxReact + DOM

Step-by-step

useEffect runs after the browser paints. Any change between the render (which read count) and the effect (which subscribes) fires no listener, so the component shows the old value until some unrelated re-render. Layout effects of other components, and messages arriving right after mount, fall exactly into this gap.

Unlock the full solution

What's inside

  • Step-by-step
  • Approach
  • Solution
  • Complexity
  • Edge cases
  • Follow-up questions
  • Tests

Includes 2 code listings, 4 edge cases, 4 follow-up questions.

One-time payment by UPI, card or net banking. Launch price.

All 150 answersOne payment, yours forever