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 でアクションを発動する