# useSyncExternalStore, Without the Mystery: Part 1
Table of Contents
Have you ever had some state sitting just outside React, then wondered why your component did not update when that state changed?
That is the exact little gap useSyncExternalStore is built for.
Most React state is boring in a good way.
You call useState, React stores the value, and your component re-renders when that value changes.
import { useState } from 'react';
function Counter() { const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;}But sometimes React is not the owner of the state.
Maybe the value lives in:
- A tiny custom store
- A state management library
- A WebSocket cache
window.locationnavigator.onLinelocalStorage
That is where useSyncExternalStore comes in.
The short version:
useSyncExternalStore lets React read and subscribe to state that lives outside React.
In this first part, we’ll keep the scope small: one tiny store, one component, and a practical look at what React roughly does internally.
The Problem
Let’s say we have a tiny counter store outside React.
let count = 0;const listeners = new Set<() => void>();
export const counterStore = { get() { return count; },
increment() { count++; listeners.forEach((listener) => listener()); },
subscribe(listener: () => void) { listeners.add(listener);
return () => { listeners.delete(listener); }; },};This store works fine by itself:
counterStore.increment();console.log(counterStore.get()); // 1But React does not know when count changes.
If a component simply reads counterStore.get(), it will not re-render when the store updates.
function Counter() { const count = counterStore.get();
return <button onClick={() => counterStore.increment()}>Count: {count}</button>;}Clicking the button changes the store, but the UI can stay stuck. React was never told to render again.
The Naive Approach
The first idea is usually useState plus useEffect.
import { useEffect, useState } from 'react';
function Counter() { const [count, setCount] = useState(counterStore.get());
useEffect(() => { return counterStore.subscribe(() => { setCount(counterStore.get()); }); }, []);
return <button onClick={() => counterStore.increment()}>Count: {count}</button>;}This is not ridiculous. For many small apps, it may appear to work.
But there is a mismatch:
- The source of truth is the external store.
- React state is now a copy of that external value.
- The subscription starts after render, inside an effect.
- React has no clear contract for asking, “What is the store value right now?”
So React gives us a hook made for this exact job.
The Hook
useSyncExternalStore takes two required functions and one optional function.
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);Here is what each piece means:
subscribetells React how to listen for store changes.getSnapshottells React how to read the current value.getServerSnapshottells React what value to use during server rendering.
If you are not doing server rendering, you can ignore the third argument for now.
The name sounds intimidating, but the idea is small:
React: "How do I read the current value?"Store: "Call getSnapshot."
React: "How do I know when it changed?"Store: "Call subscribe."That is the contract.
Using The Store
Let’s connect our counter store to React.
import { useSyncExternalStore } from 'react';import { counterStore } from './counterStore';
function Counter() { const count = useSyncExternalStore(counterStore.subscribe, counterStore.get);
return <button onClick={() => counterStore.increment()}>Count: {count}</button>;}Now React knows two things:
- How to read the current count.
- How to subscribe when the count changes.
When counterStore.increment() runs, it calls every listener. React hears that signal, calls counterStore.get() again, compares the result, and re-renders if the value changed.
No copied state. No effect in the component. No manual syncing.
Extracting A Hook
You probably do not want every component to know about subscribe and get.
So wrap it in a custom hook.
import { useSyncExternalStore } from 'react';import { counterStore } from './counterStore';
export function useCounter() { return useSyncExternalStore(counterStore.subscribe, counterStore.get);}Now the component becomes boring again.
function Counter() { const count = useCounter();
return <button onClick={() => counterStore.increment()}>Count: {count}</button>;}That boring API is the point.
What Is A Snapshot?
A snapshot is the value React reads from the external store during render.
For our counter, the snapshot is simple:
counterStore.get(); // 0, 1, 2, ...The important rule: if the store has not changed, getSnapshot should return the same value as before.
This is easy for strings, numbers, booleans, and stable objects from a store.
This is bad:
function getSnapshot() { return { count: counterStore.get() };}That creates a new object every time. React sees a different object and thinks the snapshot changed.
Better:
function getSnapshot() { return counterStore.get();}If your snapshot really needs to be an object, cache it in the store and only replace it when the underlying data changes.
What React Does Internally
Let’s make a tiny fake version.
This is not React’s real source code. It is just the useful mental model.
function useTinyExternalStore<T>( subscribe: (callback: () => void) => () => void, getSnapshot: () => T,) { const snapshot = getSnapshot();
afterReactCommits(() => { // During commit, React subscribes to future changes. return subscribe(() => { const nextSnapshot = getSnapshot();
if (!Object.is(snapshot, nextSnapshot)) { // Tell React this component needs to render again. rerender(); } }); });
beforeReactCommits(() => { const nextSnapshot = getSnapshot();
if (!Object.is(snapshot, nextSnapshot)) { // The store changed while React was rendering. rerender(); } });
return snapshot;}Real React is more careful than this, but this gets the shape right:
- Read the snapshot during render.
- Store the snapshot React just rendered with.
- Subscribe to the external store.
- When the store notifies React, read the snapshot again.
- Compare the old and new snapshots with
Object.is. - Re-render if they are different.
That explains why returning a fresh object from getSnapshot is a problem. React keeps asking, “Did the value change?” and the answer is always yes.
The Render Gap
There is one more detail worth knowing.
React does not only check the store when your subscription fires. It also does consistency checks around rendering.
Why?
Because an external store can change outside React.
Imagine this flow:
React starts rendering with count = 1External store changes to count = 2React is about to commit the UI for count = 1Without an extra check, React could commit a UI based on stale external data.
So React’s implementation keeps enough information to ask:
"Is the snapshot I rendered still the latest snapshot?"If not, React forces another render with the new value.
This is one of the reasons the hook exists. It gives React a reliable way to read the current external value and verify that the value did not change at an awkward time.
Why “Sync” Is In The Name
The word sync is there because updates from external stores are treated synchronously.
That does not mean your whole app becomes synchronous. It means React needs store subscriptions to behave in a very direct way:
Store changedNotify ReactReact reads latest snapshotReact updates if neededExternal stores usually do not have multiple in-progress versions of state for React to choose from. There is just the current store value.
So React keeps this contract strict and simple.
What We’ve Built
We built a tiny external counter store and connected it to React with useSyncExternalStore.
The hook needs two main things:
getSnapshot, so React can read the current value.subscribe, so React can re-render when the value changes.
Then we looked at the internal shape:
read snapshotsubscribeon change, read againcompare with Object.isre-render if changedReact owns the rendering. The external store owns the data. useSyncExternalStore is the bridge between them.
In Part 2, we’ll cover the stuff we skipped here: localStorage, online status, URL state, server rendering, selectors, and the rough edges that show up in real apps.