@k8ordo/state

Next.js

Next.jsのApp Routerで@k8ordo/stateを使えるようになります。サーバーで読んだ値の渡し方と、Next.jsで変わる点をたどります。

このページの内容

動くもの

app/products/state.ts
import { 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),
  }),
});

定義とuseAppStateは、Next.jsでもそのまま使えます。entryとlocalStorage、sessionStorageの状態も同じです。Cookieとメモリの状態も変わりません。

Next.jsで違うのは、サーバーでの読み取りとパスの型検査、URLを変えるupdate()の3つです。

サーバーでの読み取り

app/products/page.tsx
import 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>
    </>
  );
}

App RouterのページはsearchParamsを受け取ります。parseUrlに渡すと、urlのスキーマで読んだ値が返ります。欠けたパラメータには既定値が入り、スキーマに合わない値はそのフィールドだけが既定値に戻ります。

parseUrlが受け取る型はUrlInputです。URLSearchParamsか、Record<string, string | string[] | undefined>の形のオブジェクトです。読むときにエラーにはなりません。

hrefは純粋な関数なので、Server Componentの中でもそのまま使えます。

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>
  );
}

読んだ値は、initialUrlとしてClient Componentに渡します。サーバーの描画とハイドレーションの描画が、既定値でなくURLの値で行われます。

パスの型検査

types/k8ordo-state.d.ts
import type { Route } from 'next';

declare module '@k8ordo/state' {
  interface Register {
    path: Route;
  }
}

Next.jsにはルート表が無いので、nextのRouteをpathに登録します。アプリの中のすべてのhrefが、Next.jsの知らないパスを型エラーにします。

typedRoutesを有効にすると、Routeはアプリのパスのunionになります。hrefの返り値の型はパスをリテラルのまま残すので、Next.jsの<Link>の型検査も通ります。

Registerの拡張はアプリの中でだけ行ってください。共有のライブラリで拡張すると、その制約が使う側のすべてに及びます。

URLを変えるupdate()

Next.jsのルーターは、navigateイベントをintercept()しません。そのため、URLを変えるupdate()が呼ぶnavigation.navigate()は、ページ全体の読み込みになります。

URLの変更は、リンクとGETフォームで行ってください。リンクの作り方はリンクを見てください。History APIで代わりに書く仕組みはありません。

URLを変えないupdate()は影響を受けません。

k8ordo

Baselineに入った機能を、制限なく使うReactのライブラリ群。

© 2026 k8o — MIT License

組版:Noto Sans JP / M PLUS 2