Tearing in Concurrent UI

TL;DR / Tearing occurs when different parts of the UI read from an external store at different moments during a single concurrent render, displaying inconsistent (mixed-version) state.

How It Works

 ┌────────────┐          ┌───────────────┐
 │  Store v1  │────┐     │  Component A  │
 └────────────┘    └────→│   reads v1    │
                         └───────────────┘
                                 │
 store updates mid-render       v1
                                 ↓
 ┌────────────┐          ┌───────────────┐
 │  Store v2  │────┐     │  Component B  │
 └────────────┘    └────→│   reads v2    │
                         └───────────────┘
                                 v2
                                 └┐
                                  ↓
                       ┌────────────────────┐
                       │  UI shows v1 + v2  │
                       │      (torn!)       │
                       └────────────────────┘

Edit diagram

Tearing is a concurrency problem specific to frameworks that render non-blocking, like React 18's concurrent features. In synchronous rendering, the entire component tree renders in a single uninterruptible pass -- all components read the same version of state because nothing can change mid-render. Concurrent rendering breaks this guarantee by allowing the render to be paused, interrupted, and resumed, creating windows where external state can change between component renders.

The Mechanism

React's concurrent mode can pause rendering after Component A has read from a store but before Component B reads from the same store. During that pause, an event handler or microtask updates the store. When rendering resumes, Component B reads the updated value. The result: Component A displays version 1 of the data, Component B displays version 2, and the user sees an inconsistent UI. This is tearing.

The term comes from graphics programming, where screen tearing occurs when a display shows parts of two different frames simultaneously because the frame buffer was updated mid-scan.

Why React State Does Not Tear

React's built-in state (useState, useReducer) is immune to tearing because React controls when state transitions are applied. State updates are processed between renders, not during them. React ensures all components in a render pass see the same state snapshot. The framework batches state updates and applies them atomically before starting a new render.

Where Tearing Actually Occurs

Tearing happens with external stores -- any state managed outside of React's control. This includes Redux stores, Zustand stores, MobX observables, custom event emitters, global variables, or any mutable reference that components read during render. These stores can change at any time, including during a concurrent render, because React has no way to "lock" external mutable state.

Before React 18, this was not a problem because rendering was synchronous. The introduction of concurrent features (Suspense, useTransition, useDeferredValue, startTransition) made tearing possible in any application using external stores.

useSyncExternalStore

React 18 introduced useSyncExternalStore specifically to solve tearing. This hook takes a subscribe function, a getSnapshot function, and an optional getServerSnapshot for SSR. It guarantees that all components reading from the same store during a single render see the same snapshot value.

The key insight in its implementation: if React detects that the store changed during a concurrent render, it falls back to synchronous rendering for that update. This sacrifices the concurrency benefits (interruptibility, time-slicing) to preserve consistency. It is a deliberate tradeoff -- correctness over performance.

All major state management libraries (Redux, Zustand, Jotai, Valtio) have migrated to useSyncExternalStore or implemented equivalent tear-prevention logic. If you are using a library that has not adopted it, you are vulnerable to tearing in concurrent mode.

Detecting Tearing

Tearing is exceptionally hard to detect in testing because it requires specific timing: a store update must happen during the exact window when a concurrent render is paused. In development, React's StrictMode helps by double-rendering, but it does not simulate concurrent pauses with interleaved store updates. The React team built internal testing utilities that artificially delay renders to expose tearing, but these are not available to external developers.

In production, tearing manifests as visual glitches -- a price showing "$10" in one part of the page and "$15" in another, or a user's name being different in the header versus the sidebar. These inconsistencies are transient (the next render fixes them) and non-deterministic, making them extremely difficult to reproduce.

Beyond React

The tearing problem applies to any concurrent rendering system. Solid.js avoids it by design because it does not re-render components -- it uses fine-grained reactivity that updates DOM nodes directly. Svelte 5's runes system similarly avoids component-level re-rendering. The problem is specific to frameworks that batch component renders and can interrupt them mid-tree.

Gotchas

  • useRef values read during render can cause tearing because refs are mutable containers outside React's state management. Never read from a ref during render if its value might change concurrently.
  • useSyncExternalStore forces synchronous rendering when it detects store changes, which can negate the performance benefits of concurrent features for frequently-updating stores.
  • Tearing only occurs with concurrent features. If you never use startTransition, useDeferredValue, or Suspense for data fetching, your renders are synchronous and tearing is impossible.
  • useEffect reading from a store is not tearing -- effects run after render and always see the latest value. Tearing is specifically about inconsistent reads during a single render pass.
  • Custom hooks wrapping useRef + useEffect for subscriptions are the most common source of tearing in React 18. Replace them with useSyncExternalStore.