@k8ordo/state

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.

ts
definePageState<Url, Entry>(
  key: string,
  config: { url?: Url; entry?: Entry },
): PageState<Url, Entry>

Parameters

keystring
The state’s name: the namespace its entry values take, and the store’s registry slot.
config{ url?: StateSchema; entry?: StateSchema }
The url and entry schemas. 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 useAppState reads to pick the store.
keystring
The key passed as the first argument.
urlUrl
The url schema as passed, for @k8ordo/server’s search export or @k8ordo/form’s formFields.
entryEntry
The entry schema 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 base goes 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 url boolean is z.stringbool() and a url array defaults to []; other spellings are refused as the module loads.
  • A field declared in both url and entry is a type error and throws at runtime; so does passing neither.
  • The return type is exported as PageState.
state.ts
export 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.

ts
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/server reads the query of a page that exports search through it.

defineLocalState

Import from @k8ordo/state

Defines state in localStorage: kept until deleted, shared by every tab on the device.

ts
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 version and migrate for 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 useAppState reads 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 or null.

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 storage event.
  • The return type is exported as LocalState.

defineSessionState

Import from @k8ordo/state

Defines state in sessionStorage: kept through a reload, gone with the tab.

ts
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 useAppState reads 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 version or migrate.
  • A defineLocalState under the same key is a different state.
  • The return type is exported as SessionState.

Import from @k8ordo/state

Defines state in a cookie, which the browser writes and every request carries to the server.

ts
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 version and migrate for 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 useAppState reads 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 Map of percent-decoded values, which request.cookies under @k8ordo/server already 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=Lax and a Max-Age of 400 days; the API adds Secure.
  • 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.

ts
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 useAppState reads 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. Each update() 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.

ts
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: initialUrl only for a page state with a url slot, initialCookie only for a cookie state.

Returns

[state, update] — The current values, and the function that changes them.

Caveats

  • A page state’s values are its url and entry merged flat.
  • The server render and the hydration render show the defaults, or the values in initialUrl or initialCookie when given, or a memory state’s initial values.
  • update(patch, options?) applies on the spot and returns an UpdateHandle, with the calls of one handler going out as one write. options exists only on page state.
  • No Provider is needed: the store is created on first use.
pager.tsx
const [{ 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.

ts
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().

ts
type UpdateOptions = {
  history?: 'push' | 'replace';
};

Fields

history'push' | 'replace'
'replace' by default. 'push' adds a history entry, and only when a url value changes.

Caveats

  • No other kind’s update() takes this argument.

Versioning

Import from @k8ordo/state

The third argument of defineLocalState and defineCookieState.

ts
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. fromVersion is the version the row was written with, 0 for a row with none.

Caveats

  • What it returns passes the schema field by field.
  • A throwing migrate reads 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.

ts
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. routes wins when both are present.

Caveats

  • Under @k8ordo/static and @k8ordo/server, it is generated into .k8ordo/register.gen.ts.
  • Augment it only in an application.
  • The check is exported as RegisteredPath<Path>: Path when accepted, never when refused.

resetStateRegistry

Import from @k8ordo/state

Empties the registry of browser stores, so that state does not carry from one test into the next.

ts
resetStateRegistry(): void

Caveats

  • Unmount components first.
  • Stored rows stay; delete them by storageKey or cookieName.

OutputOf

Import from @k8ordo/state

A schema’s output type, for typing the props that carry initialUrl or initialCookie.

ts
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.

ts
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.

ts
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.

ts
type AnyState =
  | PageState
  | LocalState
  | SessionState
  | CookieState
  | MemoryState;
k8ordo

React libraries that use Baseline features without holding back.

© 2026 k8o — MIT License

Typeset in Noto Sans JP & M PLUS 2