use-stored-state is a React hook that keeps state synchronized with:
- URL query params (optional)
- Session storage or local storage (optional, mutually exclusive)
It gives you a useState-like API with persistence, hydration priority, and
validation built in.
npm install use-stored-statePeer dependency:
react >= 18
import { useStoredState } from "use-stored-state";
function Example() {
const [pageSize, setPageSize, { reset }] = useStoredState({
defaultValue: 25,
queryKey: "pageSize",
sessionStorageKey: "usersPageSize",
validValues: [10, 25, 50, 100] as const,
});
return (
<>
<label htmlFor="page-size">Users per page</label>
<select
id="page-size"
value={pageSize}
onChange={(event) => setPageSize(Number(event.target.value))}
>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
<option value={100}>100</option>
</select>
<button onClick={reset} type="button">
Reset
</button>
</>
);
}type UseStoredStateOptions<State> = {
defaultValue: State;
// Provide at least one key:
queryKey?: string;
sessionStorageKey?: string;
localStorageKey?: string;
// Validation (choose one or neither):
validValues?: readonly State[];
validate?: (value: State) => boolean;
// Parsing/serialization (provide both or neither):
parse?: (rawValue: string) => State | null;
serialize?: (value: State) => string;
};Rules:
- At least one of
queryKey,sessionStorageKey,localStorageKeyis required. sessionStorageKeyandlocalStorageKeycannot be used together.- Use
validValuesorvalidate(not both). - If you pass
parse, you must also passserialize(and vice versa). - Invalid option combinations throw at runtime (including JavaScript-only usage).
Returns:
[state, setState, { reset }];You can also destructure only [state, setState] if you do not need reset
behavior.
setState only applies valid values. reset() restores defaultValue. When
state is not already at defaultValue, it follows the normal state update and
synchronization path. When state is already at defaultValue, reset() still
re-synchronizes the configured stores so it can repair drifted query or storage
values.
Initial state is resolved in this order:
- Query param (
queryKey) - Session storage (
sessionStorageKey) or local storage (localStorageKey) defaultValue
Invalid hydrated values are ignored when validValues or validate is used.
- On mount and on each valid state update, the hook syncs current state to all configured stores (query + at most one storage source).
- At least one key is required.
sessionStorageKeyandlocalStorageKeyare mutually exclusive.- Any omitted store key is not read or written.
- If
queryKeyis set, the query param is populated on mount. - On unmount, that query param is removed.
- If multiple mounted hooks share the same
queryKey, the param is only removed after the last one unmounts.
validValues: allow-list validationvalidate: custom predicate validationdefaultValuemust pass validation, otherwise the hook throws
By default, primitive values are handled as:
boolean:"true"/"false"number:Number(rawValue)(rejectsNaN)string: unchanged
For custom state shapes, provide parse and serialize.
useKeyStore is the low-level hook used by useStoredState.
import { useKeyStore } from "use-stored-state";It syncs a single source (query, sessionStorage, or localStorage) and
returns [state, setState].
Useful commands:
npm run testnpm run lintnpm run prettiernpm run mutatenpm run type-checknpm run knipnpm run markdownlintnpm run check
Mutation testing is a required quality gate for this project.
Acceptance criteria:
- Mutation score must be
100% 0surviving mutants0timed out mutants
Recommended workflow:
- Run mutation tests while developing:
npm run mutate
- Add or improve tests until all mutants are killed.
- Re-run mutation tests to verify.
- Run the full mutation suite before opening a PR:
npm run mutate
If a mutant is equivalent and cannot be killed by a meaningful test:
- Prefer rewriting code to make intent explicit and testable.
- If still equivalent, add a targeted Stryker disable comment with a clear reason and keep the suppression as narrow as possible.