React useSyncExternalStore — Subscribing to Browser APIs Safely

Learn how to use useSyncExternalStore to safely subscribe to browser APIs and external state stores in concurrent React. Covers online status, media queries, and Redux integration.

State Hooks

Detailed Explanation

External Store Subscription with useSyncExternalStore

useSyncExternalStore provides a safe way to subscribe to external data sources in concurrent React, preventing tearing (showing inconsistent data from different points in time).

Online Status Hook

function useOnlineStatus() {
  return useSyncExternalStore(
    (callback) => {
      window.addEventListener("online", callback);
      window.addEventListener("offline", callback);
      return () => {
        window.removeEventListener("online", callback);
        window.removeEventListener("offline", callback);
      };
    },
    () => navigator.onLine,
    () => true // Server snapshot
  );
}

Media Query Hook

function useMediaQuery(query: string) {
  return useSyncExternalStore(
    (callback) => {
      const mql = window.matchMedia(query);
      mql.addEventListener("change", callback);
      return () => mql.removeEventListener("change", callback);
    },
    () => window.matchMedia(query).matches,
    () => false
  );
}

// Usage
const isDark = useMediaQuery("(prefers-color-scheme: dark)");
const isMobile = useMediaQuery("(max-width: 768px)");

The Three Arguments

  1. subscribe: Takes a callback and returns an unsubscribe function
  2. getSnapshot: Returns the current value (must return the same reference if unchanged)
  3. getServerSnapshot: Returns the value for SSR (required for server-rendered components)

Common Mistake: New Object Every Call

// BUG: Returns a new object every time, causing infinite re-renders
() => ({ width: window.innerWidth, height: window.innerHeight })

// FIX: Return a primitive, or cache the object
() => window.innerWidth // Primitive -- safe

When to Use

Use useSyncExternalStore for external state that is not managed by React: browser APIs, third-party state management libraries, or shared mutable data sources. For React-managed state, use useState or useReducer instead.

Use Case

Use useSyncExternalStore for subscribing to browser APIs (online status, media queries, geolocation), integrating external state management libraries (Redux, Zustand) with concurrent React, and reading from shared mutable data sources.

Try It — React Hooks Reference

Open full tool