ジャコ Lab

プログラミング関連のメモ帳的ブログです

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

Redux のインストールはここから

redux.js.org

$ 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 を作っていく

redux.js.org

この辺を参考に以下を作ります。

  • 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 でストアを使う

redux.js.org

この辺を参考に <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 なのでしょう。

stackoverflow.com


こちらの記事がヒットしました。

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

まとめ

完成