Next.js で Redux を使う&ハマったこと

Redux とは、状態管理モジュールの1つで、
Fulx アーキテクチャの影響を受けて作られたものです。
Vuex の React 版みたいな感じです。
Vuex は Vue 用の状態管理モジュールです。
だいたい Store とか State などの単語が登場します
Next.js プロジェクトに導入しようとしたら、
ちょっとハマったので記録に残しておきます。
開発環境・バージョン情報
この記事でのバージョン情報はこちらです
・Node.js: 20.11.1
・npm: 10.4.0
・yarn: 1.22.21
・create-next-app: 14.1.0
・@reduxjs/toolkit@2.2.1
・react-redux@9.1.0
・redux@5.0.1
・npm: 10.4.0
・yarn: 1.22.21
・create-next-app: 14.1.0
・@reduxjs/toolkit@2.2.1
・react-redux@9.1.0
・redux@5.0.1
Redux のインストールはここから
$ yarn add @reduxjs/toolkit react-redux redux
Next.js プロジェクトのディレクトリ構成
create-next-app で生成した App Router な初期構成です
. ├── README.md ├── next-env.d.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── next.svg │ └── vercel.svg ├── src │ └── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── tailwind.config.ts ├── tsconfig.json └── yarn.lock
Store や Reducer を作っていく
この辺を参考に以下を作ります。
- stores/index.ts
- stores/reducers/CountersReducer.ts を作っていきます
CountersReducer.ts
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
export interface ICounterState {
value: number;
}
const initialState: ICounterState = {
value: 0,
};
const countersSlice = createSlice({
name: "counters",
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
setValue: (state, action: PayloadAction<number>) => {
state.value = action.payload;
},
},
});
export const { increment, decrement, setValue } = countersSlice.actions;
export const countersReducer = countersSlice.reducer;
index.ts
import { configureStore } from "@reduxjs/toolkit";
import { countersReducer } from "./reducers/CountersReducer";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
export const store = configureStore({
reducer: {
counters: countersReducer,
},
});
type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
これでカウントを管理するストア完成
Next.js でストアを使う
この辺を参考に <Provider> コンポーネント を使ってあげます。
今回は Next.js の AppRouter を使用していますので、 src/app/layout.tsx あたりに入れるのが良さそうです。
[src/app/layout.tsx]
---
import { Provider } from "react-redux";
import { store } from "@/stores";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<Provider store={store}>{children}</Provider>
</body>
</html>
);
}
ハマったところ
但し、ドキュメント通りに行くと Next.js はここでエラーがでます
⨯ node_modules/react-redux/dist/rsc.mjs (29:8) @ throwNotSupportedError
⨯ Error: This function is not supported in React Server Components. Please only use this export in a Client Component.
at stringify (<anonymous>)
サーバーコンポーネントじゃ使えないよ!
Next.js はデフォルト Server Component なので layout.tsx が Server Component なのでしょう。
こちらの記事がヒットしました。
Provider を使用する Client Component を作ってあげます。
[src/stores/StoreProvider.tsx]
---
"use client";
import { ReactNode } from "react";
import { Provider } from "react-redux";
import { store } from ".";
type StoreProviderProps = {
children: ReactNode;
};
export default function StoreProvider({
children,
}: Readonly<StoreProviderProps>) {
return (
<>
<Provider store={store}>{children}</Provider>
</>
);
}
"use client";を付けると Client Component になる
layout.tsx では先程の <Provider> の変わりに <StoreProvider> を使います。
[src/layout.tsx]
---
import StoreProvider from "@/stores/StoreProvider";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<StoreProvider>{children}</StoreProvider>
</body>
</html>
);
}
ストアの使い方
[src/pages.tsx]
---
"use client";
import { useAppDispatch, useAppSelector } from "@/stores";
import { decrement, increment, setValue } from "@/stores/reducers/CountersReducer";
import { useEffect } from "react";
export default function Home() {
const { value } = useAppSelector((state) => state.counters);
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setValue(100));
}, []);
return (
<main className="min-h-screen p-24">
<div>Count: {value}</div>
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-2"
onClick={() => { dispatch(increment()); }}
>
+
</button>
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
onClick={() => { dispatch(decrement()); }}
>
-
</button>
</main>
);
}
useAppSelectorで参照するステートを得る
useAppDispatch でアクションを発動する
まとめ
