🐣

Next.js 13のApp Routerでの状態管理(nrstate編)

2023/10/15に公開

前回は・・・

前回の Next.js 13のApp Routerでの状態管理 ではZustandとURLを利用し、Client Components間、Sever Components間で状態を共有しました。今回はnrstateを利用して同じ機能を実現してみます。

今回利用したフレームワーク/ライブラリー

  • Next.js: 13.5.4
  • nrstate: 1.0.2
  • nrstate-client: 1.0.2

試しに作ってみたもの

前回と同じ機能になります。ただ、上記gifイメージを見て頂ければわかりますが、ボタン押下時に妙なブレ(?)がありますね・・・。

  • AddボタンとResetボタンをClient Componentsとして作成しています
  • その下のClient Component3にはボタン押下回数を表示してあり、Addボタン、Resetボタンと直接状態のやりとりはしておらず、nrstateを介して状態を共有しています
  • 左側のServer Component1にも同じようにボタン押下回数を表示しています。前回ここはクエリーパラメーターを利用した状態の共有を自前で実装しましたが、今回はnrstateに任せました

コード

nrstate

nrstateとは?

State for React Server Components (RSC) on Next.js の頭文字を取って N(ext)R(eact Server Components)state という名前のようで、正に探していたものという感じです。VTeacherの方が作られたようです。2023年5月10日以降修正されていない点が少し気になります。

nrstateの使い方

まず以下のようなファイルを作成します。

Counter.ts
export const path = "/";

export type Counter = {
  count: number;
};

export const initialCounter = { count: 0 } as Counter;

次に状態共有したいClient/Server Componentsの親を <PageStateProvider> で囲います。

page.tsx
import { AddButton } from "@/components/client/AddButton";
import { Display } from "@/components/client/Display";
import { ResetButton } from "@/components/client/ResetButton";
import { SideMenu } from "@/components/server/Sidemenu";
import { Counter, initialCounter, path } from "@/store/Counter";
import { currentPageState } from "nrstate";
import PageStateProvider from "nrstate-client/PageStateProvider";

export default function Home() {
  const count = 0;
  return (
    <PageStateProvider
      current={currentPageState<Counter>(initialCounter, path)}
    >
      <main className="p-24">
        <h2 className="text-xl">Server Component Page</h2>

        <div className="flex mt-10">
          <div>
            <SideMenu />
          </div>
          <div className="ml-10"></div> {/* Spacer */}
          <div>
            <AddButton />
            <div className="mt-10"></div> {/* Spacer */}
            <ResetButton />
            <div className="mt-10"></div> {/* Spacer */}
            <Display />
          </div>
        </div>
      </main>
    </PageStateProvider>
  );
}

Client Componentsでは usePageState<>() を利用し、状態と状態を更新するための関数を取得します。

AddButton.tsx
"use client";

import { Counter, path } from "@/store/Counter";
import { usePageState } from "nrstate-client";

export function AddButton() {
  const [pageState, setPageState] = usePageState<Counter>();
  const { count } = pageState;

  const onClick = () => {
    setPageState(
      {
        ...pageState,
        count: count + 1,
      },
      path
    );
  };
  // 以降省略
}

Server Componentsでは getPageState<>() を利用し、状態を取得します。

SideMenu.tsx
import { Counter, initialCounter, path } from "@/store/Counter";
import { getPageState } from "nrstate";

export function SideMenu() {
  const pageState = getPageState<Counter>(initialCounter, path);
  const { count } = pageState;

  return (
    <div className="border border-pink-400 p-10">
      <h2 className="text-xl">Server Component1</h2>
      <div className="mt-10">Count: {count}</div>
    </div>
  );
}

これだけです。自分でクエリーパラメーターを操作したり、ページ遷移する処理を書く必要もありません。簡単!

おそらく内部的にはクエリーパラメーターを利用して状態を共有しているのだと思います。ボタンを押すと http://localhost:3000/?_rsc=3z8t5 にGETが送られていたので。Counter.tsで設定した path もこれに利用されていると想像します。

今後Zustandとnrstateのどちらを利用していくか?

利用者の多さや開発の活発さを考えると当面はZustandを利用していくと思います。Server Componentsに状態を共有する必要がある場合は、その部分のみ切り出しClient Componentsにするのが現時点では無難な気がします。

Server ActionsがStableになったり、Zustand(またはその他のライブラリー)がClient Componentsで変更した状態をServer Componentsに反映する機能を持つようになれば、そのときに改めて設計を検討するつもりです。

Discussion