33
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

NextjsのApp RouterでRecoilを使う

Posted at

App Routerのコンポーネント

Nextjsのバージョン13.4からApp Routerが安定版として利用が可能になりました。
App Routerではコンポーネントをクライアントコンポーネントとサーバーコンポーネントをうまく使い分ける必要があります。
クライアントコンポーネントはファイルの先頭に'use client'と記述されたコンポーネントです。このコンポーネントはサーバー側で事前レンダリングがされ、クライアント側でコンポーネントが持つインターラクションが付与されます。
例えば以下のようなクライアントコンポーネントは

'use client';

export const SampleButton: FC = () => {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count => count + 1)}>
      {count}
    </button>
  );
};

事前レンダリングでサーバーから何の機能も持たない見た目だけのhtml要素がレンダリングされ

下のようなものが該当箇所にはめられるイメージ
<button>0</button>

同時に送信されるJavaScriptファイルを読み込むことでカウンターとしての機能を持ったボタンとして利用できます(ハイドレーション)。

一方サーバーコンポーネントはApp Routerにおいてデフォルトのコンポーネントで、サーバー上で全て組み立てられます。サーバー上で組み立てることでクライアントに送信するJavaScriptのサイズを縮小することができるのでクライアントコンポーネントだけを利用した時と比較してパフォーマンスが向上します。そのような背景があり、コンポーネントのデフォルトがサーバーコンポーネントとなり、サーバコンポーネントとして扱えないものがあれば明示的にクライアントコンポーネントとして宣言させる仕様になっていると考えられます。
サーバーコンポーネントはサーバ上で完結する必要があるので、useStateなどを用いたReactの状態管理を行えません。つまり、ReactのContextやRecoilなどを用いたコンポーネント間の状態管理についてクライアントコンポーネント間だけで動かせるような工夫が必要となります。

App RouterでのRecoilの使い方

例があるとわかりやすいので、カウンター機能を持つ以下のようなPages Routerで構成されたNext.jsアプリケーションをApp Routerに移すことを考えます。

pages/_app.tsx
import { RecoilRoot } from "recoil"

export default function MyApp({ Component, pageProps }) {
  return (
    <RecoilRoot>
      <Component {...pageProps} />
    </RecoilRoot>
  );
}
components:Counter.tsx
import { atom, useRecoilState } from "recoil"; 

const countAtom = atom({
  key: 'count',
  default: 0,
});

export const Counter: FC = () => {
  const [count, setCount] = useRecoilState(countAtom);
  return (
    <button onClick={() => setCount(count => count + 1)}>
      {count}
    </button>
  );
}
pages/counter.tsx
import { Counter } from '@/components/Counter'

export default function Home() {
  return (
    <div>
      <p>カウンター</p>
      <Counter />
      <Counter />
    </div>
  );
}

これによって出力されるのは値を共有する二つのカウンターです。
スクリーンショット 2023-05-10 19.06.48.png

まず、pages/_app.tsxを移行します。

app/layout.tsx
'use client'

import { ReactNode } from "react";
import { RecoilRoot } from "./recoil";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="ja">
      <body>
        <RecoilRoot>{children}</RecoilRoot>
      </body>
    </html>
  );
}

layout.tsxRecoilRootを使うのでクライアントコンポーネントとして定義しました。これによってApp Router内のコンポーネントでRecoilを用いた状態管理ができるようになります。
この例ではlayout.tsxを自体をクライアントコンポーネントとして扱いましたが、Providerコンポーネントを別で作ってクライアントコンポーネントを最小にすることでパフォーマンスを突き詰められます。

app/provider.tsx
'use client'

import { ReactNode } from "react";
import { RecoilRoot } from "./recoil";

export default function AppProvider({ children }: { children: ReactNode }) {
  return (
    <RecoilRoot>{children}</RecoilRoot>
  );
}
app/layout.tsx
import { ReactNode } from "react";
import { AppProvider } from "./provider";

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="ja">
      <body>
        <AppProvider>{children}</AppProvider>
      </body>
    </html>
  );
}

次に、pages/counter.tsxの移行を行います。

app/counter/page.tsx
import { Counter } from '@/components/Counter'

export default function Home() {
  return (
    <div>
      <p>カウンター</p>
      <Counter />
      <Counter />
    </div>
  );
}
components/Counter.tsx
'use client'

import { atom, useRecoilState } from "recoil"; 

const countAtom = atom({
  key: 'count',
  default: 0,
});

export const Counter: FC = () => {
  const [count, setCount] = useRecoilState(countAtom);
  return (
    <button onClick={() => setCount(count => count + 1)}>
      {count}
    </button>
  );
}

components/Counter.tsxは状態を扱うコンポーネントであるためクライアントコンポーネントとして定義します。
ここまで変更したのちに、npm run devnpm run build && npm run startをするとPages routerの時と同じようなページを取得できます(--turboでも動きます)。

まとめ

App Routerの台頭でrecoilなどを用いてコンポーネント間で行う状態管理がクライアントコンポーネントの間にあるサーバーコンポーネントによって難しくなると考えていました。

- layout(サーバーコンポーネント)
  - AppProvider(クライアントコンポーネント)
    - page(サーバーコンポーネント)
        - Counter(クライアントコンポーネント)
        - Counter(クライアントコンポーネント)

しかし今回変更したアプリケーションのように、状態管理はクライアントコンポーネントだけ扱うというところを意識さえすればこれまでと変わりなく簡単に利用できました。
これからもrecoilを使った状態管理を楽しんでいきたいです。

33
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?