useGetStateLoading saved progress…

useGetState

useGetState(initialState) is useState with a third return value: a getter that reads the newest state at the moment you call it. The state and the setter behave exactly as they always did — getState() is the addition.

It exists for callbacks that outlive the render that created them: an interval, a socket handler, an event listener registered once. Such a callback keeps reading its own render's state forever. Functional updates already fix the write side of that — setState(n => n + 1) is handed the freshest state, so a mount-only interval can count correctly without ever looking at state. Nothing fixes the read side. The moment the callback has to branch on the current state, log it, or send it somewhere, it has to actually see the value.

Signature

function useGetState<S>(
  initialState: S | (() => S),
): [S, (update: S | ((prev: S) => S)) => void, () => S];

getState must be the same function on every render, so it is safe inside a mount-only effect or a useCallback with an empty dependency array.

Examples

A mount-only interval that has to decide whether to act:

function Uploader() {
  const [queue, setQueue, getQueue] = useGetState([]);

  useEffect(() => {
    const id = setInterval(() => {
      const pending = getQueue(); // the queue as it is NOW
      if (pending.length === 0) return; // nothing to send
      send(pending);
      setQueue([]);
    }, 1000);
    return () => clearInterval(id);
  }, []); // set up once — getQueue is stable, so nothing has to re-run
}

Reading queue directly there gives you [] on every tick forever, and the uploader never sends anything.

The state and the setter are unchanged from useState:

const [count, setCount, getCount] = useGetState(0);

setCount(3); // count is 3 on the next render
setCount((n) => n + 1); // updaters work — this is useState's own setter
getCount(); // 4, without a rerender to read it back

Notes

  • getState identity never changes. Callers hold onto it inside long-lived callbacks; handing back a new function each render leaves them calling the old one, which is the bug this hook exists to fix.
  • The setter keeps useState's contract. Functional updates and a lazy initial state (useGetState(() => expensive())) both work — they are React's features, not yours, so pass the setter straight through rather than wrapping it.
  • getState() is current as of the last render. It reads state, not the setter's queue: calling setCount(1) and then getCount() on the very next line still reports the old count.
  • Falsy states are states. 0, '', false and null all have to come back out of the getter unchanged.
  • Nothing to clean up. The hook owns no timer and no subscription; it dies with the component.
Loading editor…