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.
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.
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
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.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.0, '', false and null all have to come back out of the getter unchanged.