Next.js
Use @k8ordo/state under the Next.js App Router: hand the server-read values to the client, and know what changes under Next.js.
On this page
What works as is
app/products/state.tsimport { definePageState } from '@k8ordo/state';
import * as z from 'zod';
export const listState = definePageState('product-list', {
url: z.object({
inStock: z.stringbool().default(false),
page: z.coerce.number().int().min(1).default(1),
}),
});Definitions and useAppState work under Next.js as they are. So do entry, localStorage, sessionStorage, cookie and memory state.
Three things differ under Next.js: reading on the server, typed paths, and an update() that changes the URL.
Reading on the server
app/products/page.tsximport Link from 'next/link';
import { Filters } from './filters';
import { listState } from './state';
export default async function Page({
searchParams,
}: PageProps<'/products'>) {
const url = listState.parseUrl(await searchParams);
const products = await fetchProducts(url);
const nextPage = listState.href('/products', {
...url,
page: url.page + 1,
});
return (
<>
<Filters initialUrl={url} />
<ProductList products={products} />
<Link href={nextPage}>Next page</Link>
</>
);
}An App Router page receives searchParams. Hand it to parseUrl to get the values read with the url schema: a missing parameter takes its default, and a value the schema rejects sends only that field back to its default.
parseUrl takes a UrlInput: a URLSearchParams, or an object of the shape Record<string, string | string[] | undefined>. Reading never throws.
href is a pure function, so it runs inside a Server Component as well.
app/products/filters.tsx'use client';
import { useAppState } from '@k8ordo/state';
import type { OutputOf } from '@k8ordo/state';
import { listState } from './state';
type FiltersProps = { initialUrl: OutputOf<typeof listState.url> };
export function Filters({ initialUrl }: FiltersProps) {
const [{ inStock }] = useAppState(listState, { initialUrl });
return (
<form action="/products" method="get">
<label>
<input
defaultChecked={inStock}
name="inStock"
type="checkbox"
value="true"
/>
In stock only
</label>
<button type="submit">Apply</button>
</form>
);
}Pass the values read to the Client Component as initialUrl. The server render and the hydration render then show the URL’s values, not the defaults.
Typed paths
types/k8ordo-state.d.tsimport type { Route } from 'next';
declare module '@k8ordo/state' {
interface Register {
path: Route;
}
}Next.js has no route table to hand over, so register Route from next under path. Every href in the application then turns a path Next.js does not know into a type error.
With typedRoutes enabled, Route is the union of the application’s paths. The return type of href keeps the path as a literal, so it passes the typed check of Next.js’s <Link> too.
Augment Register only in an application. A shared library that augments it imposes the constraint on every consumer.
update() that changes the URL
The Next.js router does not call intercept() on the navigate event, so the navigation.navigate() an update() makes to change the URL is a full page load.
Change the URL with links and GET forms. How to build a link is on Links. There is no History API fallback.
An update() that leaves the URL alone is unaffected.