šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

useSyncExternalStore: Reading State Outside React Safely

Understand useSyncExternalStore: safely subscribing to external state without the tearing bugs of a manual useEffect subscription.

⚔ Total XP: 0|šŸ’» react XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

External store fundamentals.

Quick Quiz //

What problem does useSyncExternalStore solve?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

Values like navigator.onLine or a third-party store's state change outside React's control, and manually syncing them with useState and useEffect can 'tear' under concurrent rendering. useSyncExternalStore solves this with a subscribe/getSnapshot contract. This lesson covers how it works and where you'll actually encounter it.

1State That Lives Outside React

Not every value a component depends on is React state — window.innerWidth, navigator.onLine, and third-party state library stores all change independently of any setState call, living in what's called an external store.

2The Naive Approach Has a Tearing Bug

A manual useState plus useEffect subscription to an external value's change events mostly works, but under React 18's concurrent rendering it can produce tearing — different parts of the UI briefly rendering inconsistent values of the same external store within a single update.

3useSyncExternalStore's Contract

useSyncExternalStore accepts a subscribe function, which registers a callback to be notified of store changes, and a getSnapshot function, which returns the store's current value. React uses this contract to read the external store safely and consistently, even during concurrent rendering, fully eliminating tearing.

4getSnapshot Must Return a Stable Reference

getSnapshot must return the exact same reference (for objects and arrays) when the underlying value hasn't actually changed. Returning a freshly created object on every call, even with identical contents, causes React to believe the store changed on every render, resulting in an infinite re-render loop.

5Where You'll Actually Encounter This

Most developers never call useSyncExternalStore directly — it's the mechanism state management libraries like Redux, Zustand, and Jotai use internally to safely connect their external stores to React components without tearing under concurrent rendering.

6Step-by-Step Breakdown

State That Lives Outside React. Not all state your components care about is managed by React. window.innerWidth, a browser's online/offline status, a third-party state library, or a value shared across multiple React roots on the same page all live in an 'external store' that changes on its own, outside of any setState call.

The Naive Approach Has a Tearing Bug. You might try useState + useEffect with a manual event listener to sync external state. This mostly works, but under React 18's concurrent rendering, it can produce 'tearing' — different parts of the UI briefly showing inconsistent values of the same external store during a single render pass.

What subtle bug can a manual useState + useEffect subscription to external state suffer from under React's concurrent rendering?

  • →Tearing — different parts of the UI briefly showing inconsistent values
  • →It's guaranteed to leak memory in every case

useSyncExternalStore's Contract. useSyncExternalStore takes two functions: a subscribe function that registers a callback to be notified of changes, and a getSnapshot function that returns the current value. React uses these to read the store safely and consistently, even during concurrent rendering, eliminating the tearing problem entirely.

getSnapshot Must Return a Stable Reference. getSnapshot should return the exact same value (by reference, for objects/arrays) if nothing has actually changed. Returning a brand-new object every call — even with identical contents — tricks React into thinking the store changed on every render, causing an infinite re-render loop.

Why must getSnapshot return the same reference when the underlying value hasn't changed?

  • →A new reference every call makes React think the store changed, causing an infinite render loop
  • →It has no real effect — it's purely a style preference

Where You'll Actually Encounter This. Most React developers never call useSyncExternalStore directly — state management libraries like Redux, Zustand, and Jotai use it internally to safely connect their external stores to React. Understanding it helps you know why those libraries are tear-free under concurrent rendering, and lets you build your own safe external-store hook when you need one.

Mastery Achieved. You now understand useSyncExternalStore: why manually syncing external state with useState/useEffect can tear under concurrent rendering, how subscribe and getSnapshot solve that safely, why getSnapshot must return a stable reference, and where you'll actually encounter this hook in practice — mostly inside the state libraries you already use. Next, you'll learn the rare, low-level useInsertionEffect hook.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported since React 18.

FirefoxSupported

Fully supported since React 18.

SafariSupported

Fully supported since React 18.

EdgeSupported

Fully supported since React 18.

Accessibility (A11y)

1Tearing-Free State Prevents Inconsistent Announced Content

If two parts of the page announce inconsistent live status (like one saying 'online' and another 'offline' from the same underlying value), it directly confuses assistive technology users — useSyncExternalStore's consistency guarantee prevents exactly this class of bug.

SEO Implications

  • 1

    useSyncExternalStore Is a Client-Only Concern

    It addresses consistency during client-side concurrent rendering and has no bearing on server-rendered HTML content, though it does accept an optional third argument for a server snapshot to support SSR use cases correctly.

Best Practices

Keep getSnapshot Cheap and Synchronous

getSnapshot may be called frequently by React during rendering — avoid expensive computation inside it, and never perform async work, since it must return a value synchronously.

Prefer an Established State Library Over Hand-Rolling useSyncExternalStore

Unless building a genuinely custom external-store integration, reach for a library like Zustand that already implements this pattern correctly, rather than re-deriving subscribe/getSnapshot logic from scratch.

Frequent Bugs

THE BUG

A component using useSyncExternalStore enters an infinite render loop.

THE FIX

getSnapshot is returning a newly created object or array on every call instead of a stable reference. Return a primitive value, or cache and reuse the same object reference until the underlying value genuinely changes.

THE BUG

Two sibling components briefly show different values for what should be the same piece of external state.

THE FIX

This is the tearing bug that a manual useState + useEffect subscription is prone to under concurrent rendering. Replace it with useSyncExternalStore, which guarantees a consistent value is read across a single render pass.

Real-World Examples

A Tear-Free Online/Offline Status Hook

An app needs multiple components to reliably agree on whether the browser is currently online, even during concurrent rendering. Implementing useOnlineStatus with useSyncExternalStore, backed by the browser's online/offline events and navigator.onLine, guarantees every component reading it sees a consistent value within the same render.

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

function getSnapshot() {
  return navigator.onLine;
}

function useOnlineStatus() {
  return useSyncExternalStore(subscribe, getSnapshot);
}

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

getSnapshot creating a new object or array on every call

// Wrong function getSnapshot() { return { online: navigator.onLine }; } // Correct function getSnapshot() { return navigator.onLine; }

The Solution //

Return a primitive value where possible, or maintain and return a cached, stable object reference that's only replaced when the underlying value genuinely changes — never construct a fresh object inline inside getSnapshot.

The Error //

Forgetting to unsubscribe inside the function returned from subscribe

function subscribe(callback) { window.addEventListener('online', callback); return () => window.removeEventListener('online', callback); // required cleanup }

The Solution //

The subscribe function must return a cleanup function that removes the event listener, or the subscription will leak and continue firing after the component unmounts.

Lesson Glossary

[01]useSyncExternalStore

A hook for safely subscribing to state managed outside React, without tearing under concurrent rendering.

Code Preview
useSyncExternalStore(subscribe, getSnapshot)

[02]External Store

State that lives and changes outside of React's own state management, like browser APIs or a state library.

Code Preview
navigator.onLine

[03]Tearing

A bug where different parts of the UI briefly show inconsistent values of the same underlying state.

Code Preview
Header ≠ Footer, same source

[04]subscribe

The function passed to useSyncExternalStore that registers a callback for store change notifications.

Code Preview
subscribe(callback)

[05]getSnapshot

The function passed to useSyncExternalStore that synchronously returns the store's current value.

Code Preview
getSnapshot() => navigator.onLine

Continue Learning