Skip to solution
hardFrontend

What is "tearing" in concurrent React?

439 views
01

Understand the problem

Question presented to candidate: "We migrated to React 18 and turned on transitions. Now, occasionally, two parts of the same screen show different values for the same piece of global state. What is happening, and how do you fix it?"

What a strong answer should cover:

  • Tearing: a single committed screen displaying two different values of the same source of truth.
  • Why React 18 made it possible: concurrent rendering can interrupt and resume a render, so a mutable external value can change mid-render.
  • Why React's own state is immune — it is snapshotted per render — while an external store read directly in render is not.
  • useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) as the official fix, and that it deliberately forces a synchronous, non-interruptible re-render.
  • getSnapshot must return a cached value; returning a fresh object each call is an infinite loop.
  • getServerSnapshot exists for SSR, where there is no store to subscribe to.
  • That modern libraries (Redux, Zustand, Jotai) already call this internally — you rarely write it by hand.

Clarifying questions expected:

  • "Is the state in a React store or an external one — a module variable, a Redux store, a browser API?"
  • "Are we using transitions or Suspense anywhere near this subtree?"

Code / implementation expected: Yes — a useSyncExternalStore call with a correct, cached getSnapshot.

reactconcurrentstate-management
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Frontend engineers preparing for senior React interviews — assumes familiarity with hooks and React 18 concurrency. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The store-lifecycle and failure-mod

Solution ready — 2 min read

Classified // press E to declassify

04

Explore the playground snippets

useSyncExternalStore with a correctly cached snapshot
Run Playground
import { useSyncExternalStore, startTransition, useState } from "react";

// A tiny external store — mutable state living entirely outside React.
function createStore(initial) {
  let state = initial;
  const listeners = new Set();
  return {
    // The snapshot is the state object itself, replaced only on write. This is
    // what makes getSnapshot cacheable: same data means the SAME reference.
    getSnapshot: () => state,
    subscribe: (listener) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
    set: (next) => {
      state = next;                 // new object -> Object.is sees a change
      listeners.forEach((l) => l());
    },
  };
}

const store = createStore({ count: 0 });

// WRONG, for contrast — a fresh object every call makes Object.is always false,
// so React re-renders forever:
//   const bad = () => ({ count: store.getSnapshot().count });

function Panel({ name }) {
  // The third argument is required for server rendering: on the server there is
  // nothing to subscribe to, so React needs a value it can render into HTML.
  // Omit it and server rendering throws "Missing getServerSnapshot".
  const snap = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
  return (
    <div style={{ border: "1px solid #ccc", borderRadius: 6, padding: 12, minWidth: 120 }}>
      <div style={{ fontSize: 12, color: "#666" }}>{name}</div>
      <strong style={{ fontSize: 22 }}>{snap.count}</strong>
    </div>
  );
}

export default function App() {
  const [, force] = useState(0);
  const bump = () => store.set({ count: store.getSnapshot().count + 1 });

  return (
    <div style={{ padding: 24, fontFamily: "system-ui" }}>
      <div style={{ display: "flex", gap: 12 }}>
        <Panel name="Panel A" />
        <Panel name="Panel B" />
      </div>
      <p>
        <button onClick={bump}>Increment store</button>{" "}
        {/* Even inside a transition, this update commits synchronously and
            both panels always agree — that is the anti-tearing guarantee. */}
        <button onClick={() => startTransition(bump)}>Increment in a transition</button>{" "}
        <button onClick={() => force((n) => n + 1)}>Re-render parent</button>
      </p>
      <p style={{ color: "#666", fontSize: 13 }}>
        Both panels read the same store independently and can never disagree,
        because React re-checks the snapshot before it commits.
      </p>
    </div>
  );
}
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 103 of 119 decoded in the React.js track. One more won't hurt.

Back to track