Here, we look at the current query after loading the hints, and only show the hints we loaded if the query has not changed since. Not the most elegant solution, but it works, and so it's a valid state model. If you were to split query and hints into separate states, you'd lose the ability to read current query when setting hints, and have to solve this problem some other way.
More generally (maybe too generally), if updates to state B depend on state A, states A and B should probably we wrapped in an object.
I promised you can have all the convenience of class setState
in a custom hook. Here we go:
function useObjectState(init) {
return useReducer((s, patch) => {
const changed = Object.entries(patch)
.some(([k, v]) => s[k] !== v);
return changed ? { ...s, ...patch } : s;
}, init);
}
Here, we merge old and new state, and also preserve the old state object reference if the patch contains no changes. Easy breezy.
For a tie-breaker, let's see if the amount of useState
calls impacts your application performance.
I expect the runtime performance difference between single object state and multiple atomic states to be even more negligible than that of bundle size. Still, the fact that it could go both ways is made me curious: object state allocates an extra object (or function, with a lazy initializer) on every render, but atoms call more react internals. Is there a winner?
I've made a tiny benchmark comparing several useState calls, single useState(object)
call and single useState(() => lazy object)
. The results are available in a google sheet. I've also made a nice chart that shows percent increase in mount time over baseline — no hooks, just a stateless render:
I wouldn't dare interpret these results given how cool optimizing compilers are, but the general as I see it pattern makes sense:
useState
with atom is slightly better than with object because we skip object allocation.useState
calls are more expensive than object allocations, so for 3+ items useState(object)
wins.Note that the difference here is in sub-microsecond range (yes, MICROsecond, 1/1000th of a millisecond, or 1/16000th of a 60FPS frame), so any practical implications are laughable. Still, good to know that using hooks is almost free.
So, useState is probably better suited for storing atomic values, but object state still has its uses. Here's what we learnt:
useState
update handle skips re-render by checking for ===
equality, and that's easier to achieve with atomic values.useState
has no built-in object merging mechanism.useStates
result in useless renders. Use object state or unstable_batchedUpdates
to render once.useState
in a state update callback (ouch, that's a complex statement with many states involved) — use object state for values that depend on each other during update.useState
variants is negligible.I feel the deciding factor here is state modelling — grouping several state items in an object signals that they are closely related, while splitting them apart shows they are orthogonal. Please model your state based on common sense, not some prejudices agains objects. Ah, and also — everything we just discussed also applies to useReducer
, because useState
is useReducer
. Good luck and see you next time!