Recoil が Next.js v13 でも使えるのか調べた話
結論
現時点では使えなさそうでした。
調査結果は以下に示しています。ソースコードやエラーログも載せているので少し長めになっています。
v13 の Recoil を使う時の変更点と注意点
v13 ではまだ beta 版のものですが、ページを配置するディレクトリが/pages から/app に変更されました。
app ディレクトリでは今まで pages ディレクトリにはなかった機能が多く追加されています。その中で Recoil を使うあたって重要なのは Server Component が追加されたことです。app ディレクトリ配下では Server Component がデフォルトとなっていて、コンポーネントの配備の仕方が難化しています。
Server Component は名前の通りサーバー側でレンダリング処理を行います。そのため、ブラウザ側に状態をキャッシュしておくような React Hooks などは使用できません。そういうものを使う場合はファイルの頭にuse client
と記述して Client Component 化する必要があります。Recoil は useContext や useStateなどのような機能を持ち合わせており、それらと同様に状態を保持しておく役割を持っているため、Recoil を使用するコンポーネントは Client Component にしなくてはなりません。
また、Next.js v12 までは Recoil を使う際は_app.tsx 内で component をRecoilRoot
で囲う必要がありました。v13 では_app.tsx と_document.tsx が統合されて layout.tsx になったため、置く場所がどうしたらいいのかよくわからない状態になっています。ですが、公式ドキュメントのRendering third-party context providers in Server Components
でその解決策が示されていたので、今回はそれを踏襲しています。
実行環境
- Windows10
- Google Chrome
- Node.js v18.12.0
- yarn v1.22.19
Create Next App
最低限必要な環境のみで今回の検証をやってみるつもりなので、create next-app
で新規にプロジェクトを作ります。
yarn create next-app --ts
そして Recoil もインストールします。
yarn add recoil
依存関係は次のようになります。
"dependencies": {
"next": "13.0.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"recoil": "^0.7.6"
},
"devDependencies": {
"@types/node": "18.11.8",
"@types/react": "18.0.24",
"@types/react-dom": "18.0.8",
"eslint": "8.26.0",
"eslint-config-next": "13.0.0",
"typescript": "4.8.4"
}
コードを書く
簡単なコードで、2 ページ構成です。動作は以下の 3 つのです。
- input 要素に文字列を入れて register を押すと Recoil に値が格納される。
- UserPage のリンクを押すとページが遷移し、次のページで Recoil に格納されている値が表示される。
- return のリンクを押すと最初のページに戻る。再入力可能。
ディレクトリごとにコードを示していきます。ルートのページはつぎのようになります。
// app/layout.tsx
import { Providers } from "./provider";
const RootLayout = ({ children }: { children: React.ReactNode }) => {
return (
<html lang="en">
<head>
<title>Recoil Sample</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
};
export default RootLayout;
// app/provider.tsx
("use client");
import { RecoilRoot } from "recoil";
export const Providers = ({ children }: { children: React.ReactNode }) => {
return <RecoilRoot>{children}</RecoilRoot>;
};
// app/page.tsx
import Link from "next/link";
import { SetUserName } from "./setRecoilState";
const Page = () => {
return (
<main>
<h1>NextPage</h1>
<Link href="/user">UserPage</Link>
<SetUserName />
</main>
);
};
export default Page;
// app/setRecoilState.tsx
("use client");
import { userFullNameState } from "@/atom/state";
import { useSetRecoilState } from "recoil";
export const SetUserName = () => {
const setUserName = useSetRecoilState(userFullNameState);
const handleClick = () => {
const inputNode = document.getElementById("inputName") as HTMLInputElement;
const name = inputNode.value;
setUserName(name);
};
return (
<div>
<h1>set recoil</h1>
<form>
<input type="text" name="name" id="inputName" />
<button type="button" onClick={handleClick}>
register
</button>
</form>
</div>
);
};
なお Recoil の state は以下のようになります。
// atom/state.ts
import { atom } from "recoil";
export const userFullNameState = atom<string>({
key: "userFullNameState",
default: "",
});
/user ページは次のようになります。
// app/user/layout.tsx
const UserPageLayout = ({ children }: { children: React.ReactNode }) => {
return <main>{children}</main>;
};
export default UserPageLayout;
// app/user/page.tsx
import Link from "next/link";
import { GetUserName } from "./getRecoilState";
const UserPage = () => {
return (
<div>
<h1>UserPage</h1>
<GetUserName />
<Link href="/">return</Link>
</div>
);
};
export default UserPage;
// app/user/getRecoilState.tsx
("use client");
import { useRecoilValue } from "recoil";
import { userFullNameState } from "@/atom/state";
export const GetUserName = () => {
const userName = useRecoilValue(userFullNameState);
return (
<div>
<h1>recoil</h1>
<p>from recoil:{userName}</p>
</div>
);
};
build する
エラーが発生する。
info - Creating an optimized production build
info - Compiled successfully
info - Linting and checking validity of types
info - Collecting page data
[= ] info - Generating static pages (0/4)TypeError: Cannot read properties of undefined (reading '')
at m (C:\try-to-next-v13-app\node_modules\next\dist\compiled\react-server-dom-webpack\client.js:917:81)
at Q (C:\try-to-next-v13-app\node_modules\next\dist\compiled\react-server-dom-webpack\client.js:927:80)
at S (C:\try-to-next-v13-app\node_modules\next\dist\compiled\react-server-dom-webpack\client.js:928:317)
at c (C:\try-to-next-v13-app\node_modules\next\dist\compiled\react-server-dom-webpack\client.js:930:310)
Error occurred prerendering page "/user". Read more: https://nextjs.org/docs/messages/prerender-error
TypeError: Cannot read properties of undefined (reading '')
at m (C:\try-to-next-v13-app\node_modules\next\dist\compiled\react-server-dom-webpack\client.js:917:81)
at Q (C:\try-to-next-v13-app\node_modules\next\dist\compiled\react-server-dom-webpack\client.js:927:80)
at S (C:\try-to-next-v13-app\node_modules\next\dist\compiled\react-server-dom-webpack\client.js:928:317)
at c (C:\try-to-next-v13-app\node_modules\next\dist\compiled\react-server-dom-webpack\client.js:930:310)
[== ] info - Generating static pages (1/4)TypeError: Cannot read properties of undefined (reading '')
at m (C:\try-to-next-v13-app\node_modules\next\dist\compiled\react-server-dom-webpack\client.js:917:81)
at Q (C:\try-to-next-v13-app\node_modules\next\dist\compiled\react-server-dom-webpack\client.js:927:80)
at S (C:\try-to-next-v13-app\node_modules\next\dist\compiled\react-server-dom-webpack\client.js:928:317)
at c (C:\try-to-next-v13-app\node_modules\next\dist\compiled\react-server-dom-webpack\client.js:930:310)
(node:23112) ExperimentalWarning: The Fetch API is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
[ ===] info - Generating static pages (3/4)TypeError: Cannot read properties of undefined (reading '')
at Oa (C:\try-to-next-v13-app\.next\server\chunks\125.js:989:56)
at Ia (C:\try-to-next-v13-app\.next\server\chunks\125.js:1102:50)
at Array.toJSON (C:\try-to-next-v13-app\.next\server\chunks\125.js:879:32)
at stringify (<anonymous>)
at S (C:\try-to-next-v13-app\.next\server\chunks\125.js:1177:51)
at ping (C:\try-to-next-v13-app\.next\server\chunks\125.js:978:43)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
TypeError: Cannot read properties of undefined (reading '')
at Oa (C:\try-to-next-v13-app\.next\server\chunks\125.js:989:56)
at Ia (C:\try-to-next-v13-app\.next\server\chunks\125.js:1102:50)
at Array.toJSON (C:\try-to-next-v13-app\.next\server\chunks\125.js:879:32)
at stringify (<anonymous>)
at S (C:\try-to-next-v13-app\.next\server\chunks\125.js:1177:51)
at ping (C:\try-to-next-v13-app\.next\server\chunks\125.js:978:43)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
Error occurred prerendering page "/". Read more: https://nextjs.org/docs/messages/prerender-error
TypeError: Cannot read properties of undefined (reading '')
at m (C:\try-to-next-v13-app\node_modules\next\dist\compiled\react-server-dom-webpack\client.js:917:81)
at Q (C:\try-to-next-v13-app\node_modules\next\dist\compiled\react-server-dom-webpack\client.js:927:80)
at S (C:\try-to-next-v13-app\node_modules\next\dist\compiled\react-server-dom-webpack\client.js:928:317)
at c (C:\try-to-next-v13-app\node_modules\next\dist\compiled\react-server-dom-webpack\client.js:930:310)
info - Generating static pages (4/4)
> Build error occurred
Error: Export encountered errors on following paths:
/page: /
/user/page: /user
at C:\try-to-next-v13-app\node_modules\next\dist\export\index.js:405:19
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async Span.traceAsyncFn (C:\try-to-next-v13-app\node_modules\next\dist\trace\trace.js:79:20)
at async C:\try-to-next-v13-app\node_modules\next\dist\build\index.js:1263:21
at async Span.traceAsyncFn (C:\try-to-next-v13-app\node_modules\next\dist\trace\trace.js:79:20)
at async C:\try-to-next-v13-app\node_modules\next\dist\build\index.js:1123:17
at async Span.traceAsyncFn (C:\try-to-next-v13-app\node_modules\next\dist\trace\trace.js:79:20)
at async Object.build [as default] (C:\try-to-next-v13-app\node_modules\next\dist\build\index.js:64:29)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
散々調べましたが、理由がわからず…。起動するわけないよね、と思いながらもnext dev
で実行してみる。
next dev さん、なぜか動く。が…
なぜかちゃんと動いている…。developer tool やコンソールにもエラーは出てきていないよう。
しかし、ページのリロードを行うと以下のようなエラーが出てきます。
Unhandled Runtime Error
Error: Cannot read properties of undefined (reading 'SetUserName')
Call Stack
resolveModuleMetaData
webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js (195:82)
serializeModuleReference
webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js (1298:50)
resolveModelToJSON
webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js (1660:40)
Array.toJSON
webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js (1081:40)
stringify
<anonymous>
processModelChunk
webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js (163:36)
retryTask
webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js (1823:50)
performWork
webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js (1856:33)
AsyncLocalStorage.run
node:async_hooks (330:14)
eval
webpack-internal:///(sc_server)/./node_modules/next/dist/compiled/react-server-dom-webpack/server.browser.js (1934:55)
Recoil に値を格納するコンポーネントが見つからないと言ってます。さらに developer tool のほうのコンソールには以下のようなエラーも出ていました。
The above error occurred in the <NotFoundErrorBoundary> component:
at NotFoundErrorBoundary (webpack-internal:///./node_modules/next/dist/client/components/layout-router.js:316:9)
at NotFoundBoundary (webpack-internal:///./node_modules/next/dist/client/components/layout-router.js:323:11)
at LoadingBoundary (webpack-internal:///./node_modules/next/dist/client/components/layout-router.js:234:11)
at ErrorBoundary (webpack-internal:///./node_modules/next/dist/client/components/error-boundary.js:40:11)
at RenderFromTemplateContext (webpack-internal:///./node_modules/next/dist/client/components/render-from-template-context.js:12:34)
at OuterLayoutRouter (webpack-internal:///./node_modules/next/dist/client/components/layout-router.js:17:11)
at RecoilRoot_INTERNAL (webpack-internal:///./node_modules/recoil/es/index.js:4452:3)
at RecoilRoot (webpack-internal:///./node_modules/recoil/es/index.js:4618:5)
at Providers (webpack-internal:///./app/provider.tsx:10:11)
at body
at html
at ReactDevOverlay (webpack-internal:///./node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:53:9)
at HotReload (webpack-internal:///./node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:19:11)
at Router (webpack-internal:///./node_modules/next/dist/client/components/app-router.js:73:11)
at ErrorBoundaryHandler (webpack-internal:///./node_modules/next/dist/client/components/error-boundary.js:28:9)
at ErrorBoundary (webpack-internal:///./node_modules/next/dist/client/components/error-boundary.js:40:11)
at AppRouter
at ServerRoot (webpack-internal:///./node_modules/next/dist/client/app-index.js:114:11)
at RSCComponent
at Root (webpack-internal:///./node_modules/next/dist/client/app-index.js:130:11)
React will try to recreate this component tree from scratch using the error boundary you provided, ReactDevOverlay.
考察(根拠はまだ出せてないので、ほぼ私の推測です)
今の Next.js が内部的にどんな処理をしているのかあまりよくわかっていないので、色々調べはしたもののはっきりと原因はこれだと言えないです。再レンダリングが正常に動作しない、というのは動作とログではっきりとわかりますが。。。
おそらく SetUserName が undefined だから読めないよー、というのと<NotFoundErrorBoundary>コンポーネントでエラーが起きているよーというのは同じ意味だと思われます。正常に読み取れなかったコンポーネントは内部的には<NotFoundErrorBoundary>コンポーネントに置き換えられている…?ということなのでしょうか。
また、Recoil を使っているコンポーネントが読めないと言ってますし、エラーログにがっつり RecoilRoot が出てきているので、このエラーの根本原因は Recoil であることは確かですね。
少し別の観点からですが、今のコンポーネントの構造は、
- layout(Server Component)
- provider(Client Component)
- page(Server Component)
- Recoil set or get(Client Component)
ですが、そもそもこの Server と Client とでサンドイッチしている状態があまり良くないのかな、となんとなく考えています。公式ドキュメントでは、Client Component はなるべく末端に配置したほうがパフォーマンスはいいからおすすめよ、と言っています。裏を返せば上位コンポーネントを Client Component にすると内部的に処理がとても複雑になるということだと思われます。複雑化した結果、バグが発生した?ということかもしれません。やっぱりサンドイッチにするのはアンチパターンなのかもしれないですね(Context Provider の話をしているとことは言ってることが真逆ですが…)。
ちなみに、もしかしたらパッケージがいかれたのかとも思ったので、一度 Recoil の記述部分を全部消して next build と next dev をしましたが、何も問題なく実行されてリロードしても全く問題なかったので、Recoil が原因であるのは確かだと思われます。また、yarn と Node.js も入れなおして、パッケージも全部入れなおして、.next ディレクトリも消して、全部まっさらな状態でやり直したりもしましたが、結果は変わらずでした。
まとめ
Recoil は現状 Next.js v13 の app ディレクトリ以下では使えないようでした。原因はいまいちよくわからないので、究明するには Client side と Server side のそれぞれの処理のされ方をもう少し勉強する必要が出てきました。
最後にですが、私は今年からIT業界に入ったため知識が大変少ないです。上に載せているソースコードがそもそも間違っているので、このエラーが出ていたのかもしれません。もし、間違ってるぞという場合はご指摘いただけると幸いです。