📚

[Next.js + Recoil] ページ全体で使う値を store に保存しつつ SSR にも対応したい

に公開

コードから

_app.tsx
import type { AppProps, AppContext } from 'next/app';
import React from 'react';
import { RecoilRoot } from 'recoil';

import { getUser } from '@/fetcher/user';
import type { User } from '@/fetcher/user';
import { isSSR } from '@/utils/common';
import { userState } from '@/stores/user';

type PropsInGetInitialProps = {
  inGetInitialProps: {
    user: User;
  } | null;
};

type Props = AppProps & PropsInGetInitialProps;

const App = ({
  Component,
  pageProps,
  inGetInitialProps
}: Props) => (
  <RecoilRoot
    initializeState={({ set }) => {
      if (inGetInitialProps) {
        set(userState, inGetInitialProps.user);
      }
    }}
  >
    <Component {...pageProps} />
  </RecoilRoot>
);

App.getInitialProps = async (
  appContext: AppContext
): Promise<PropsInGetInitialProps> => {
  if (!isSSR(appContext.ctx)) {
    return {
      inGetInitialProps: null
    };
  }

  const user = await getUser();
  return {
    inGetInitialProps: {
      user,
    }
  };
};

export default App;
utils/common.ts
import type { IncomingMessage } from 'http';

export const isSSR = (ctx: { req?: IncomingMessage }) => {
  const isServer = !!ctx.req;
  const isNextLinkNavigation = !!ctx.req?.headers['x-nextjs-data'];
  return isServer && !isNextLinkNavigation;
};
stores/user.ts
import { atom, useRecoilValue, useSetRecoilState } from 'recoil';

import type { User } from '@/fetcher/user';

export const userState = atom<User | null>({
  key: 'user',
  default: null
});

export const useRecoilUser = () => useRecoilValue(userState);
export const useSetRecoilUser = () => useSetRecoilState(userState);
pages/some_page/index.tsx
...
import { useRecoilUser } from '@/stores/user';
...
const SomePage: FC = () => {
  ...
  const user = useRecoilUser();
  ...

ちょっと解説

_app.tsx のコードについて

App.getInitialProps = async (
  appContext: AppContext
): Promise<PropsInGetInitialProps> => {
  if (!isSSR(appContext.ctx)) {
    return {
      inGetInitialProps: null
    };
  }

  const user = await getUser();
  return {
    inGetInitialProps: {
      user,
    }
  };
};

getInitialProps は非推奨ですが、アプリルートでは getServerSideProps が使えないので仕方なく使っています。

getInitialProps はサーバーでもクライアントでも呼び出されるので、サーバーサイド且つ初回訪問時のサーバー処理時にのみ fetch するようにしています。( isSSR については次のセクションにて)
その値を props で渡し、 props に値がある時にのみ RecoilRoot の初期値として使う、という流れです。
これで fetch と recoil の初期値設定は1度しか呼ばれません。

isSSR について

import type { IncomingMessage } from 'http';

export const isSSR = (ctx: { req?: IncomingMessage }) => {
  const isServer = !!ctx.req;
  const isNextLinkNavigation = !!ctx.req?.headers['x-nextjs-data'];
  return isServer && !isNextLinkNavigation;
};

サーバーサイドかどうか?については context の req を見ればわかります。
「初回訪問時のサーバー処理時にのみ」を判定する方法ですが、 context の中を見てそれっぽい値で判定しているのですが、これについては参考記事を見かけたことがなくて、あまり自信がありません。
今だけ動くコード( "next": "14.1.4" にて確認)の可能性が高いです。。。

続編

[編集中]
[Next.js + Recoil] SSR 時に1度だけサーバーで fetch して、あとは Recoil の値を参照したい
に続きます。

Discussion