createGlobalStateLoading saved progress…

createGlobalState

createGlobalState(initialValue) returns a hook that behaves like useState, except every component calling it shares one value. You call the factory once at module scope; the value it holds lives in a closure, outside React, and any component can read it and write it without a prop or a provider in sight.

That last part is the whole problem. React re-renders in response to its own state setters. A variable in a module closure can change a thousand times and nothing on screen moves, because nothing told React. So a store outside React needs three things wired by hand: a way to read the current value, a way to subscribe to changes, and a way to tell React that a subscribed component is now out of date.

This is the smallest question in the catalog that has all three. No selectors, no reducers, no atoms — one value, shared.

Signature

function createGlobalState<S>(initialValue: S): () => [
  S,
  (next: S | ((prev: S) => S)) => void,
];

Examples

Call the factory once, at module scope, and export the hook:

// store.js
const useCount = createGlobalState(0);

Then any component uses it like useState — with no provider and no props:

function Counter() {
  const [count, setCount] = useCount();
  return <button onClick={() => setCount((n) => n + 1)}>{count}</button>;
}

// A sibling. Nothing connects it to Counter.
function Badge() {
  const [count] = useCount();
  return <span>{count}</span>; // clicking the button moves this
}

Notes

  • One store per call. Two createGlobalState calls are two independent values. The factory is the store; the hook is a window onto it.
  • useState's contract. setValue takes a value or an updater (prev) => next, and its identity is stable, so it is safe in a dependency array.
  • The store is not component state. It outlives every subscriber. Unmount the whole tree, mount a fresh component, and the value is still whatever it was.
  • Mounting late is normal. A component that first renders after the value has moved must show the current value, not the initial one.
  • Unmounting unsubscribes. A component that has left must not be updated, and must not keep the store from collecting it.
  • Out of scope — selectors, equality functions, reducers, and persistence. One value, read and written whole.

FAQ

Why does a value in a module variable not re-render anything?
React re-renders in response to its own state setters, not to a variable changing. A module-scope value can change a thousand times and no component moves, because nothing told React. Bridging that gap is what useSyncExternalStore does.
Do I need useSyncExternalStore, or is useState plus a listener set enough?
The hand-rolled version works — react-use ships exactly that shape today and it passes every behavioural test here. What it cannot do is survive concurrent rendering: if the store changes partway through a render pass it can commit two components showing different values of the same store. That is tearing, and it is the reason the hook exists.
Why does React crash with Maximum update depth exceeded?
Your getSnapshot returns a new object every call. React calls it on every render and compares with Object.is, so a fresh object looks like a change every time and it re-renders forever. Return a cached value — the stored object itself — and only build a new one when the store actually changes.
How is this different from Context?
Context passes a value down through the tree, so every consumer re-renders when the provider value changes and the value only exists inside the provider. This store lives outside React entirely, needs no provider, survives every subscriber unmounting, and can be read and written by code that is not a component at all.
Loading editor…