Next.jsにReduxを導入するステップ [Next15 React19]

当ページのリンクには広告が含まれている場合があります。

Reactでプロジェクトを作るにあたり、状態管理:Stateとしてよく利用されるRedux。

今回、Next.jsのプロジェクトにおいてReduxを導入したので、使い始めるまでの手順をまとめました。

Qiita、YouTube、Udemyとすでに導入ステップを紹介しているものはありますが、現在の開発環境や私なりに整理したいディレクトり構成などでうまくマッチするものがなかったので、現環境でズバリなものとして残していきます。

開発環境

  • Next.js:15.0.4
  • React:19.0.0※
  • react-redux:9.1.2 / @reduxjs/toolkit:2.4.0

npx create-next-app@latestで同時にインストールされるバージョン

目次

Nextプロジェクトの作成

まずは、いつも通りにNext.jsのプロジェクトを作成します。

npx create-next-app@latest

設定は基本デフォルトのままです。

√ What is your project named? ... my-app-name
√ Would you like to use TypeScript? ... Yes
√ Would you like to use ESLint? ... Yes
√ Would you like to use Tailwind CSS? ... Yes
√ Would you like your code inside a `src/` directory? ... Yes
√ Would you like to use App Router? (recommended) ... Yes
√ Would you like to use Turbopack for next dev? ... No
√ Would you like to customize the import alias (@/* by default)? ... No

作成したプロジェクトに移動して、VS Code(Cursor)を立ち上げます。

cd my-app-name
code .

Reduxのインストール

次に、Reduxをインストールしていきます。

ここで最初の躓きポイントがありました。私が作成した時点では、ReduxがReact19に対応していません。

まだReact19が正式ではないとのことですが、Nextアプリ作った時点で決まっているのでこれは困ったものです。

その内、React19に対応するとは思いますが、現時点で取れる手段は二つ

  • React18にバージョンを落として、インストールする。
  • -- fource オプションを付けて強制的にインストールする。

今回は、-- fourceオプションで対応しました。

まだ、プロジェクトにおいてもReduxを使い始めたばかりですので、どこまで影響あるかは未確認です。何か致命的な問題があれば、バージョンを落として対応しようと思います。

npm install react-redux @reduxjs/toolkit --force
...略
npm warn node_modules/react
npm warn   peer react@"^18.0" from react-redux@9.1.2
npm warn   node_modules/react-redux
npm warn     react-redux@"*" from the root project
npm warn     1 more (@reduxjs/toolkit)

added 8 packages, and audited 381 packages in 6s

138 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

いくつか警告が出ますが、インストール自体は無事完了。

個人的には、UIの部分でshadcn/uiも使っていてこちらも--force オプション付けているので、どうせだったらはバージョン落としてもいいかなとも思いました。

Reactのバージョンを落とす時

Next15 はReact18もサポートしているので、デフォルトでインストールされるReact関連のライブラリをバージョン落としてからReduxをインストールすることも可能です。

npm install react@18 react-dom@18 @types/react@18 @types/react-dom@18

react関連は、型も含め4つのライブラリがあるため、インストールしなおす。

ReduxのReact19対応について

Next.jsでは現時点でプロジェクトを新しく作るとReact19がインストールされますが、まだ"react": "^19.0.0" のバージョンからわかるように、安定ではないようです。

ReduxのIssueからはなぜ、React19に対応しないのかという問いに対して、「まだReact19はリリースしていない」とする回答が記載されています。

なお、このIssueは日付的にも2024年8月のものですが、つい最近ようやくReact19の安定板がリリースされたようです。Reduxのインストールに少し戸惑ったのも時間の問題かもしれないですね。

Reduxを各種ファイルの設定

さて、少し脱線しましたが、ここからReduxを使うための具体的なステップを紹介してきます。

Reduxの仕組みとかはさておき、ひとまずこのままやれば使い始めることが出来るという想定で作成していきます。

ディレクトリの作り方など、やや違いがありますが、公式ドキュメントにもNextで始めるためのガイドラインあります。

/storesの作成

まずは、状態管理関連のファイル等を集めるために /srcディレクトリ下に/storesディレクトリを作成します。

store.tsの作成

公式ドキュメントのチュートリアルにしたがい、store.ts/storesディレクトリ下に作成します。

counterは後から作成する状態管理変数です。ひとまずこの時点でコピペしておいてください。

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './Counter/slice'

export const makeStore = () => {
  return configureStore({
    reducer: {
      counter: counterReducer
    }
  })
}

// Infer the type of makeStore
export type AppStore = ReturnType<typeof makeStore>
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<AppStore['getState']>
export type AppDispatch = AppStore['dispatch']

hooks.tsの作成

色々な導入事例などを参照してるけど、hooks.tsがどこで参照されているか把握できてないです。ひとまず、公式チュートリアルに従って作っていきます。

hooks.ts/storesディレクトリ下に作成します。

import { useDispatch, useSelector, useStore } from 'react-redux'
import type { AppDispatch, AppStore, RootState } from './store'

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
export const useAppStore = useStore.withTypes<AppStore>()

StoreProvider.tsxの作成

Reduxによる状態管理変数をプロジェクト内で利用できるようするには、<Provider />タグでアプリ全体を包括する必要があります。

基本的にはアプリ全体で利用するものになるので、layout.tsxなどに設定すればいいかと思いますが、ここで注意が必要です。

状態管理はクライアント側での動作が基本となるため、use clientを付けて、クライアントコンポーネントにしておきます。layout.tsxはサーバー側の設定のままにしておくので、StoreProviderを分けて作成しておきます。

'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore, AppStore } from './store'

export default function StoreProvider({
  children
}: {
  children: React.ReactNode
}) {
  const storeRef = useRef<AppStore | null>(null)
  if (!storeRef.current) {
    // Create the store instance the first time this renders
    storeRef.current = makeStore()
  }

  return <Provider store={storeRef.current}>{children}</Provider>
}

‘use client’を記載して、クライアントコンポーネントとして、定義したtsxファイルです。

tsxファイルが、appやcomponentsや配下にないと気に食わないとかであれば移動してもいいでしょう(笑)。ひとまず、Store関連はまとめることが今は分かりやすいかなと思ってます。

Counter/slice.tsの作成

最後に、状態管理する変数とその変数を操作する関数(Reducer)の設定です。

状態変数ごとにファイル分けたほうが管理しやすいかなと思って、/stores/{状態変数名}/slice.tsという名称で統一して作成することにします。あまりに数が多いのであればそのうち整理するかもしれない、、、

今回は、特定のボタンをクリックするたびにカウントアップまたはカウントダウン、さらに特定の引数を与えてその数値分カウントアップするチュートリアル的なものにしておきます。

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

// stateの型定義
interface CounterState {
  value: number;
}
// strateの初期値
const initialState: CounterState = {
  value: 5
}
// state管理の本体
const counterSlice = createSlice({
  name: "counter",  // 名称 ※分かりやすければ何でもいい
  initialState,     // 初期値の定義 initalState: 5とか記載してもいい
  reducers: {       // stateを更新する処理群
    increment(state) {
	    // stateの加算を行う
      state.value++;
    },
    decrement(state) {
	    // stateの減算を行う
      state.value--;
    },
    amountAdd(state, action: PayloadAction<number>){
	    // 第二引数にactionとして数値を受け取り、数値分加算する。.payloadとして値を取得する
      state.value += action.payload;
    },
  },
})
// reducersをcounterSlice.actionsからexport
export const { increment, decrement, amountAdd } = counterSlice.actions;
// export defaultとして、counterSlice.reducerを指定
export default counterSlice.reducer;

これで、counterを状態変数として追加する処理が出来ました。

状態変数を追加する場合

状態変数を追加する場合は、/stores/{状態変数名}/slice.ts として新たに定義して、その後にstore.tsのreducerに変数を追記します。

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './Counter/slice'
// 新たな状態変数をimport
import newStateReducer from './NewState/slice'

export const makeStore = () => {
  return configureStore({
    reducer: {  
      counter: counterReducer
      // reducerに追記
      newState: newStateReducer
    }
  })
}

// Infer the type of makeStore
export type AppStore = ReturnType<typeof makeStore>
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<AppStore['getState']>
export type AppDispatch = AppStore['dispatch']

/app/layout.tsxの更新

ここからは、/storesディレクトリではなく、/appディレクトリ内の修正をしていきます。

まずは、設定した状態変数をプロジェクト全体で利用するために、layout.tsxに先ほど作成したStoreProviderコンポーネントである<StoreProvider />タグで全体を囲みます。

import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
// StoreProviderをimport
import StoreProvider from "@/stores/StoreProvider";

// 以下は初期フォントとかなどで必要に応じて削除
const geistSans = localFont({
  src: "./fonts/GeistVF.woff",
  variable: "--font-geist-sans",
  weight: "100 900",
});
const geistMono = localFont({
  src: "./fonts/GeistMonoVF.woff",
  variable: "--font-geist-mono",
  weight: "100 900",
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
	{/* StorePrvederタグで全体を閉じる */}
    <StoreProvider>
      <html lang="en">
        <body
          className={`${geistSans.variable} ${geistMono.variable} antialiased`}
        >
          {children}
        </body>
      </html>
    </StoreProvider>
  );
}

コンポーネントからの呼び出し

最後に作成した状態変数counterをコンポーネントから呼び出して動くか確認してみます。

初期作成の/app/page.tsxのreturn 文を一度削除して、ここにボタンと数値をとりあえず埋め込みます。

// 状態変数を用いるコンポーネントはクライアントコンポーネント
'use client'
// react-reduxの必要な関数を呼び出し
import { useDispatch, useSelector } from "react-redux";
// 状態変数counterに定義したreducersをimport
import { amountAdd, decrement, increment } from "@/stores/Counter/slice";
// store.tsに作成したRootState(getState)をimport
import { RootState } from "@/stores/store";

export default function Home() {
	// 状態変数はstate.{変数名}.valueで取得
  const counter = useSelector((state: RootState) => state.counter.value)
  const dispatch = useDispatch()
  return (
    <>
      <div>
	      {/* 各種処理をクリックで実行する */}
	      <span>{counter}</span>
        <button aria-label="Increment value" onClick={() => dispatch(increment())}>
          Increment
        </button>
        {/* action.payloadとして第二引数が必要なものは値を入力 */}
        <button aria-label="Amount add value" onClick={() => dispatch(amountAdd(2))}>
          amountAdd
        </button>
        <button aria-label="Decrement value" onClick={() => dispatch(decrement())}>
          Decrement
        </button>
      </div>
    </>
  );
}

これで状態管理として使えるようになりました。

後は、プロジェクトに合わせて修正・追加していく。

まとめ

今回、作成した後のディレクトリ構成はこのような感じになりました。

状態管理が増えるたびに/storesディレクトリ下が肥大していくので、プロジェクト進めながら一つディレクトリ挟むかもしれません。

また、reduxのインストールでも触れてますが、現時点でreact19に対応していないので、正式に対応した後はバージョンをあげようかと思っています。

もしくは、react関連の4つのライブラリを18にバージョン落とすか。どちらにせよreduxがReact19に対応したらバージョンあげたくなるし、現時点では--forceでインストールしたそのままとしておきます。

Next.js×Reduxは公式にもチュートリアルあるし、Qiita・YouTube(海外)・ブログなどにもいくつかありまいたが現在の環境でズバリこの通りというものがありませんでした。ただ、これらを参考にした程度ですので、先人の皆様には感謝です。

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

日本語が含まれない場合は表示できません(スパム対策)

目次