Client state — persistStore, urlState, dropzone#
Three browser-side helpers from janux/client: state that survives a reload, state that lives in the URL, and files coming in from the user.
import { persistStore, urlState, dropzone } from 'janux/client';persistStore(store, config?)#
Mirrors a store's state into storage and restores it on boot.
import { persistStore } from 'janux/client';
import { theme } from './stores';
persistStore(theme, {
name: 'my-app:theme',
partialize: (state) => ({ mode: state.mode }), // don't persist transient fields
version: 2,
migrate: (persisted, from) => (from < 2 ? { mode: persisted.dark ? 'dark' : 'light' } : persisted),
});PersistConfig |
Default | Notes |
|---|---|---|
name |
janux:store:<store name> |
Storage key |
storage |
localStorage |
Any StateStorage: getItem / setItem / removeItem, sync or async — so sessionStorage, IndexedDB wrappers and native bridges all fit |
partialize |
persist everything | Pick the slice worth keeping. Transient flags (isLoading, open dialogs) should stay out |
version |
0 |
Bump when the shape changes |
migrate |
— | (persisted, from) => state. Runs when the stored version is older; without it, incompatible payloads are dropped rather than crashing your boot |
Persisted payloads are versioned envelopes, so a shipped rename can't leave a user stuck with state your code no longer understands.
urlState(name, type, fallback, options?)#
Puts a piece of UI state in the query string: shareable, deep-linkable, back/forward-correct.
import { urlState } from 'janux/client';
import { str } from 'janux';
const tag = urlState('tag', str(), 'all');
tag.value.value; // reactive read (a signal)
tag.set('display'); // writes ?tag=display- Schema-validated. The raw param is parsed (JSON when it starts with
[or{) and validated against the schema type. Anything invalid falls back — a hand-edited URL can't corrupt your state. replace: trueby default. Filter and tab changes shouldn't fill the history stack. Pass{ replace: false }when a change is a navigation step the user should be able to go back through.- Back/forward aware. It listens to
popstateand re-reads the param, so the signal always matches the address bar. - Query-only changes are shallow — same path, different search means no page re-render and no server round-trip; islands react through this handle. See Data cache & URL state.
dropzone(options)#
Everything needed to accept files, without writing drag-and-drop plumbing.
import { dropzone } from 'janux/client';
const zone = dropzone({
accept: ['image/*', 'application/pdf'],
multiple: true,
maxSize: 5_000_000,
onFiles: (files) => upload(files),
});
const detach = zone.attach(hostElement); // drag & drop, paste, click-to-pick
zone.isOver.value; // reactive: style the drop target
zone.open(); // open the native picker yourselfDropzoneOptions |
Notes |
|---|---|
accept |
MIME list; image/* wildcards supported |
multiple |
Defaults to false |
maxSize |
Bytes; oversized files are filtered out |
onFiles |
Called with the files that passed the filters — never called with an empty list |
attach(el) returns a detach function; wire it up in an attach lifecycle and call it in detach, or let the ownership scope do it. Filtering happens before onFiles, so a rejected file never reaches your code. Pair it with an HTTP handler for the upload endpoint.
Related: Stores · Data cache & URL state · HTTP handlers & uploads