API
The functions and types @k8ordo/state exports. useAppState is a hook for Client Components; every other function runs on the server and the client alike.
On this page
definePageState
Import from @k8ordo/state
Defines page state: url in the URL’s query, entry in the history entry’s hidden state.
definePageState<Url, Entry>(
key: string,
config: { url?: Url; entry?: Entry },
): PageState<Url, Entry>Parameters
keystring- The state’s name: the namespace its
entryvalues take, and the store’s registry slot. config{ url?: StateSchema; entry?: StateSchema }- The
urlandentryschemas. Pass at least one.
Returns
PageState<Url, Entry> — A pure definition that both server and client code import.
Fields
kind'page'- The kind of definition, which
useAppStatereads to pick the store. keystring- The key passed as the first argument.
urlUrl- The
urlschema as passed, for@k8ordo/server’ssearchexport or@k8ordo/form’sformFields. entryEntry- The
entryschema as passed. parseUrl(input: UrlInput) => OutputOf<Url>- Reads the url slot: defaults for missing parameters, and a field the schema rejects falls back to its default. It never throws.
href(path, values?) => string- Builds a link. A field left out means its default, fields at their default are left out of the query, and Vite’s
basegoes in front. search(values?) => string- Returns the query string alone, without the
?.
Caveats
- Every field needs a
.default()or an.optional(); without one, the module throws as it loads. - A
urlboolean isz.stringbool()and aurlarray defaults to[]; other spellings are refused as the module loads. - A field declared in both
urlandentryis a type error and throws at runtime; so does passing neither. - The return type is exported as
PageState.
state.tsexport const listState = definePageState('product-list', {
url: z.object({
page: z.coerce.number().int().min(1).default(1),
}),
entry: z.object({
expanded: z.array(z.string()).default([]),
}),
});urlReader
Import from @k8ordo/state
Builds, from a url schema alone, a function that reads the way parseUrl does, for code handed the schema without its definition.
urlReader<Url extends StateSchema>(
schema: Url,
): (input: UrlInput) => output<Url>Parameters
schemaStateSchema- A url schema.
Returns
(input: UrlInput) => output<Url> — A function that reads a query. The reader is built once, when urlReader is called.
Caveats
@k8ordo/serverreads the query of a page that exportssearchthrough it.
defineLocalState
Import from @k8ordo/state
Defines state in localStorage: kept until deleted, shared by every tab on the device.
defineLocalState<Schema>(
key: string,
schema: Schema,
versioning?: Versioning<Schema>,
): LocalState<Schema>Parameters
keystring- The state’s name. The localStorage key becomes
k8ordo-state:<key>. schemaStateSchema- The schema of the stored values. Every field needs a
.default()or an.optional(). versioningVersioning- The
versionandmigratefor a stored shape that changed. Without it, a row is the bare values object.
Returns
LocalState<Schema> — The local state’s definition.
Fields
kind'local'- The kind of definition, which
useAppStatereads to pick the store. keystring- The key passed as the first argument.
schemaSchema- The schema as passed.
storageKeystring- The localStorage key the values are written under:
k8ordo-state:<key>. inlineRead() => string- A JavaScript expression for an inline
<script>, which evaluates in the browser to the stored object ornull.
Caveats
- Values are stored as JSON, so keep fields to what JSON holds, and the schema must accept its own output.
- Other tabs’ writes arrive through the
storageevent. - The return type is exported as
LocalState.
defineSessionState
Import from @k8ordo/state
Defines state in sessionStorage: kept through a reload, gone with the tab.
defineSessionState<Schema>(
key: string,
schema: Schema,
): SessionState<Schema>Parameters
keystring- The state’s name. The sessionStorage key becomes
k8ordo-state:<key>. schemaStateSchema- The schema of the stored values. Every field needs a
.default()or an.optional().
Returns
SessionState<Schema> — The session state’s definition.
Fields
kind'session'- The kind of definition, which
useAppStatereads to pick the store. keystring- The key passed as the first argument.
schemaSchema- The schema as passed.
storageKeystring- The sessionStorage key the values are written under:
k8ordo-state:<key>. inlineRead() => string- A JavaScript expression for an inline
<script>, reading sessionStorage.
Caveats
- It takes no
versionormigrate. - A
defineLocalStateunder the same key is a different state. - The return type is exported as
SessionState.
defineCookieState
Import from @k8ordo/state
Defines state in a cookie, which the browser writes and every request carries to the server.
defineCookieState<Schema>(
key: string,
schema: Schema,
versioning?: Versioning<Schema>,
): CookieState<Schema>Parameters
keystring- The state’s name. The cookie is named
k8ordo-state.<key>, so the key may hold only letters, digits and the symbols an HTTP token allows. schemaStateSchema- The schema of the stored values. Every field needs a
.default()or an.optional(). versioningVersioning- The
versionandmigratefor a stored shape that changed. Without it, a row is the bare values object.
Returns
CookieState<Schema> — The cookie state’s definition.
Fields
kind'cookie'- The kind of definition, which
useAppStatereads to pick the store. keystring- The key passed as the first argument.
schemaSchema- The schema as passed.
cookieNamestring- The name of the cookie the values are written under:
k8ordo-state.<key>. parseCookies(cookies: ReadonlyMap<string, string>) => output<Schema>- Reads the values out of a request’s cookies, given as a
Mapof percent-decoded values, whichrequest.cookiesunder@k8ordo/serveralready is. An older row is migrated as it is read, but never written back. cookieValue(values?) => string- The value for a server writing the same cookie: the JSON of the values after the schema, unencoded. A field left out means its default.
Caveats
- The browser writes it with the Cookie Store API, as
Path=/,SameSite=Laxand aMax-Ageof 400 days; the API addsSecure. - It can never be
HttpOnly, so never put a secret in it. - Over 4 KB, name and value together, the write’s handle rejects.
- The return type is exported as
CookieState.
defineMemoryState
Import from @k8ordo/state
Defines a typed shared box in the JavaScript runtime, which goes back to its initial values on reload.
defineMemoryState<Values>(
key: string,
initial: Values,
): MemoryState<Values>Parameters
keystring- The state’s name, used as the store’s registry slot.
initialValues- The initial values. They fix the type and the set of keys, and are what the server renders.
Returns
MemoryState<Values> — The memory state’s definition.
Fields
kind'memory'- The kind of definition, which
useAppStatereads to pick the store. keystring- The key passed as the first argument.
initialReadonly<Values>- A copy of the initial values.
Caveats
- There is no schema, because the values never leave the runtime.
- Replace values through
update(); never mutate them. Eachupdate()applies on the spot, unbatched. - The return type is exported as
MemoryState.
useAppState
Import from @k8ordo/state
Returns a definition’s current values and the update that changes them. One Client Component hook for every kind of definition.
useAppState(def, options?): [state, update]
useAppState(def, keys, options?): [Pick<state, Key>, update]Parameters
defAnyState- The definition to read and update.
keysreadonly Key[]- The keys to subscribe to; only their changes re-render.
[]subscribes to nothing. options{ initialUrl? } | { initialCookie? }- What the server read:
initialUrlonly for a page state with a url slot,initialCookieonly for a cookie state.
Returns
[state, update] — The current values, and the function that changes them.
Caveats
- A page state’s values are its
urlandentrymerged flat. - The server render and the hydration render show the defaults, or the values in
initialUrlorinitialCookiewhen given, or a memory state’s initial values. update(patch, options?)applies on the spot and returns anUpdateHandle, with the calls of one handler going out as one write.optionsexists only on page state.- No Provider is needed: the store is created on first use.
pager.tsxconst [{ page }, update] = useAppState(listState, ['page']);
update({ page: page + 1 }, { history: 'push' });UpdateHandle
Import from @k8ordo/state
What update() returns: an object holding two promises, the same shape as navigation.navigate()’s.
type UpdateHandle = {
committed: Promise<void>;
finished: Promise<void>;
};Fields
committedPromise<void>- Settles once the write is in its place.
finishedPromise<void>- Settles once whatever the router does after the write is done too.
Caveats
- Ignoring it trips no lint, and its rejections stay quiet.
- An overtaken navigation rejects with an
AbortError, and a failed write with its error.
UpdateOptions
Import from @k8ordo/state
The second argument of a page state’s update().
type UpdateOptions = {
history?: 'push' | 'replace';
};Fields
history'push' | 'replace''replace'by default.'push'adds a history entry, and only when aurlvalue changes.
Caveats
- No other kind’s
update()takes this argument.
Versioning
Import from @k8ordo/state
The third argument of defineLocalState and defineCookieState.
type Versioning<Schema> = {
version: number;
migrate: (
old: Readonly<Record<string, unknown>>,
fromVersion: number,
) => { readonly [Key in keyof input<Schema>]?: unknown };
};Fields
versionnumber- The current version, a positive integer.
migrate(old, fromVersion) => values- Turns an older version’s values into the current shape.
fromVersionis the version the row was written with,0for a row with none.
Caveats
- What it returns passes the schema field by field.
- A throwing
migratereads as nothing stored, and leaves the row alone.
Register
Import from @k8ordo/state
The interface an application augments so that the paths href takes are checked by type.
declare module '@k8ordo/state' {
interface Register {
routes: typeof routes;
}
}Fields
routestypeof routes- The type of
@k8ordo/router’s route table; paths are matched against its patterns segment by segment. pathstring- The path union of a router with no table.
routeswins when both are present.
Caveats
- Under
@k8ordo/staticand@k8ordo/server, it is generated into.k8ordo/register.gen.ts. - Augment it only in an application.
- The check is exported as
RegisteredPath<Path>:Pathwhen accepted,neverwhen refused.
resetStateRegistry
Import from @k8ordo/state
Empties the registry of browser stores, so that state does not carry from one test into the next.
resetStateRegistry(): voidCaveats
- Unmount components first.
- Stored rows stay; delete them by
storageKeyorcookieName.
OutputOf
Import from @k8ordo/state
A schema’s output type, for typing the props that carry initialUrl or initialCookie.
type OutputOf<Schema> = Schema extends StateSchema
? output<Schema>
: Record<never, never>;Caveats
- Hand it a definition’s schema, as in
OutputOf<typeof listState.url>. For an absent slot (undefined) it is an empty object type.
StateSchema
Import from @k8ordo/state
The schema type definitions take: the common ground of zod’s and zod/mini’s z.object(), so a schema written with either fits.
type StateSchema<Shape extends $ZodShape = $ZodShape> =
$ZodObject<Shape> & { shape: Shape };UrlInput
Import from @k8ordo/state
The query parseUrl and the function from urlReader take: a URLSearchParams, or the object shape frameworks hand a page.
type UrlInput =
| URLSearchParams
| Readonly<Record<string, string | readonly string[] | undefined>>;AnyState
Import from @k8ordo/state
The union of every kind of definition, for typing a helper that takes any of them.
type AnyState =
| PageState
| LocalState
| SessionState
| CookieState
| MemoryState;