A tiny, type-safe toolkit for persisting React state to different backends using declarative schemas. Define how each field is serialized/deserialized once, and reuse the same schema with:
- LocalStorage
- URL Search Params
- Custom storages
Full TypeScript inference included.
- π Type-safe schemas with
DataType<T>
(serialize/deserialize per-field) - πΎ Multiple backends: localStorage, URL query, or your own
- βοΈ React hooks:
usePersistentState
,useLocalStorageState
,useSearchParamsState
- π§© Default values supported per-field
- π¦ Utility fns for direct read/write without hooks
npm i @sasha.p/use-persistent-state
Defines how a single field is persisted:
export type DataType<T, D = null> = {
serialize(value: NonNullable<T>): string;
deserialize(serializedValue?: string | null | undefined): T | D;
};
T
β runtime type you want to work withD
β fallback type when there is no value (defaults tonull
)deserialize
should return either a value ofT
or the fallbackD
Use the provided factory helper to build consistent data types with optional defaults:
export const createDataTypeCtr = <T>(
impl: (options?: { defaultValue?: T }) => DataType<T, any>,
) => {
/* ... */
};
This yields curried creators like $String
, $Number
, etc., each supporting:
- No options β fallback is
null
{ defaultValue }
β fallback is that value
$String
β persists strings$StringArray
β persists string arrays$Number
β persists numbers$NumberArray
β persists number arrays$Boolean
β persists booleans
Example with defaults:
const name = $String({ defaultValue: 'John' });
const age = $Number({ defaultValue: 30 });
const tags = $StringArray({ defaultValue: [] });
Schemas describe the shape of your persistent state:
const schema = {
name: $String({ defaultValue: 'Guest' }),
age: $Number(),
tags: $StringArray({ defaultValue: [] }),
};
This produces a fully typed object when loaded.
Backend-agnostic hook that powers all others:
const [state, setState] = usePersistentState(schema, {
get: (key) => storage.getItem(key),
save: (data) => storage.setItem('myKey', JSON.stringify(data)),
});
state
β typed object matching schemasetState(update)
β updates and persists values
Persist state in localStorage:
const schema = {
theme: $String({ defaultValue: 'light' }),
count: $Number({ defaultValue: 0 }),
};
const [state, setState] = useLocalStorageState(schema, { key: 'app-settings' });
// Usage
console.log(state.theme); // "light"
setState({ theme: 'dark' });
Persist state in URL query params:
const schema = {
page: $Number({ defaultValue: 1 }),
filter: $String(),
};
const [state, setState] = useSearchParamsState(schema);
// Usage
console.log(state.page); // 1 (from ?page=1)
setState({ page: 2 }); // updates URL to ?page=2
Read persisted values directly:
const values = getPersistedValues(schema, { key: 'app-settings' });
Write values directly:
persistValues(schema, { theme: 'dark', count: 5 }, { key: 'app-settings' });
Implement the Storage
interface to create your own backend:
export interface Storage<Key = string> {
get(key: Key): string | null;
save(data: object): void;
}
Example: sessionStorage adapter:
const sessionStorageAdapter: Storage<string> = {
get: (key) => sessionStorage.getItem(key),
save: (data) => sessionStorage.setItem('state', JSON.stringify(data)),
};
const schema = {
username: $String({ defaultValue: 'Anonymous' }),
darkMode: $Boolean({ defaultValue: false }),
};
function App() {
const [state, setState] = useLocalStorageState(schema, { key: 'settings' });
return (
<div>
<p>Hello, {state.username}</p>
<button onClick={() => setState({ darkMode: !state.darkMode })}>
Toggle Dark Mode
</button>
</div>
);
}
MIT Β© Your Name