📚
[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