10LOC
Reactadvanced

useSyncExternalStore for tearing-free external store subscriptions

Published July 19, 2026

import { useSyncExternalStore } from "react";

const EVENTS = ["online", "offline"] as const;

const subscribe = (onStoreChange: () => void) => {
  EVENTS.forEach((event) => window.addEventListener(event, onStoreChange));
  return () => EVENTS.forEach((event) => window.removeEventListener(event, onStoreChange));
};

const getSnapshot = () => navigator.onLine;
const getServerSnapshot = () => true;

export const useOnlineStatus = () =>
  useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);

What

useSyncExternalStore lets a component read state that lives outside React — a browser API, a Redux-style store, a WebSocket connection — and re-render whenever that state changes, without ever showing a torn (inconsistent) snapshot across components during a concurrent render.

Why it matters

Before this hook, subscribing to external state meant useEffect + useState: subscribe in an effect, call setState on every change. That works for one component in isolation, but under React's concurrent rendering, two components reading the same external store at slightly different points in a single render pass can each commit a different snapshot of it — a "tear." useSyncExternalStore exists specifically to close that gap: React calls getSnapshot() during render and again after committing, forcing a synchronous re-render if the store changed in between.

navigator.onLine is a textbook case. It's real mutable state that lives on window, not in React state, and several components — a connectivity banner, a sync indicator, a disabled submit button — might all need to agree on its value at the same instant.

How it works

The snippet above breaks into the three arguments useSyncExternalStore takes:

  • subscribe registers a change listener and returns an unsubscribe function. React calls it once per mounted component and cleans it up automatically — no manual useEffect needed.
  • getSnapshot returns the current value. React calls it on every render (and again after every store change) to decide whether a re-render is needed. It must return a value that's Object.is-stable when nothing actually changed, or you'll get a render loop.
  • getServerSnapshot covers server rendering, where navigator doesn't exist. Returning true (assume online) avoids a hydration crash; the client re-checks the real value on mount.

Gotchas

  • getSnapshot runs on every render, including renders React later discards — keep it cheap and synchronous.
  • Don't construct a new object or array inside getSnapshot when the underlying value hasn't changed. That breaks the Object.is check and causes a re-render storm. navigator.onLine is a primitive boolean, so it's not an issue here, but it's the first thing to check with richer external stores.
  • This hook is for state that genuinely lives outside React. If it already lives in useState, useReducer, or context, you don't need it.

Related

  • useTransition — keeps React-owned state responsive under load; a different tool for a related "avoid janky UI" goal.
  • useDeferredValue — same goal as useTransition, applied to a value instead of a transition.