この記事では、Next.jsとTypeScriptを使用して、ページを跨いだ状態管理ができるReduxの実装方法を説明する。
参考にした公式ドキュメント: https://redux.js.org/usage/nextjs
環境
"typescript": "^5"
"next": "14.0.3",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0",
"react-hook-form": "^7.48.2",
"react-redux": "^9.1.0",
"@reduxjs/toolkit": "^2.2.3",
手順
依存関係の追加
npm install @reduxjs/toolkit react-redux
ファイル構成
プロジェクトのファイル構成は、Reduxの状態管理ロジックを/lib
ディレクトリに、Reactコンポーネントを/app
と/pages
ディレクトリに格納。
以下の構成でファイルを作成する。
/src
/app
- layout.rsx ← 従来存在するレイアウトファイル
- StoreProvider.tsx ← 作成
/lib
- store.ts
- hooks.ts
/ features
/counter
- counterSlice.ts
/pages
/test
- redux.tsx ←ストアに接続し、データの更新を行う
- index.tsx ←トップページ(ストアに接続し、別のページで更新されたストアのデータにアクセスできるかを確認)
- _app.tsx
lib/store.ts
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./features/counter/counterSlice";
export const store = () => {
return configureStore({
reducer: {
counter: counterReducer,
},
});
};
// Infer the type of store
export type AppStore = ReturnType<typeof store>;
// ストア自体から `RootState` 型と `AppDispatch` 型を推測する
export type RootState = ReturnType<AppStore["getState"]>;
export type AppDispatch = AppStore["dispatch"];
store.ts
はReduxストアを設定する。configureStore
メソッドを使用して、アプリケーションのストアを作成し、counter
という名前の状態管理のためのcounterReducer
を含める(後術)。
この設定により、アプリケーション全体でcounter
の状態を管理できる。
lib/hooks.ts
import { useDispatch, useSelector, useStore } from "react-redux";
import type { AppDispatch, AppStore, RootState } from "./store";
// 単一の `useDispatch` と `useSelector` の代わりに、アプリ全体で使用する。
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
export const useAppStore = useStore.withTypes<AppStore>();
hooks.ts
は、ReduxのuseDispatch
、useSelector
、useStore
フックをアプリケーションの型でラップし、再利用可能にする。これにより、型安全な方法でReduxの機能をReactコンポーネント内で使用できる。
lib/features/counter/counterSlice.ts
import { RootState } from "@/lib/store";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
// slice stateの型を定義する
export interface CounterState {
value: number;
}
// 型を使って初期状態を定義する
const initialState: CounterState = {
value: 0,
};
export const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
// PayloadAction型を使用して、`action.payload`の内容を宣言する
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// セレクタなどの他のコードは、インポートされた `RootState` 型を使用することができる
export const selectCount = (state: RootState) => state.counter.value;
export default counterSlice.reducer;
counterSlice.ts
は、カウンターの状態とロジックを管理するRedux ToolkitのcreateSlice
を使用している。ここで、カウンターの値を増減させるアクションと初期状態を定義。
app/StoreProvider.tsx
"use client";
import { AppStore, store } from "@/lib/store";
import { useRef } from "react";
import { Provider } from "react-redux";
export default function StoreProvider({
children,
}: {
children: React.ReactNode;
}) {
const storeRef = useRef<AppStore>();
if (!storeRef.current) {
// 最初のレンダリング時にストアインスタンスを作成する
storeRef.current = store();
}
return <Provider store={storeRef.current}>{children}</Provider>;
}
StoreProvider.tsx
は、ReduxのProvider
コンポーネントを使用して、アプリケーションのトップレベルでReduxストアを提供する。これにより、アプリケーションのどのコンポーネントからもReduxストアにアクセスできるようになる。
pages/_app.tsx
import type { AppProps } from "next/app";
import StoreProvider from "@/app/StoreProvider";
export default function App({ Component, pageProps }: AppProps) {
return (
<StoreProvider>
<Component {...pageProps} />
</StoreProvider>
);
}
_app.tsx
は、Next.jsの特別なページで、アプリケーションで使用されるすべてのページに共通のレイアウトや設定を適用する。ここでStoreProvider
を使って、すべてのページでReduxストアが利用できるようにしている。
pages/test/redux.tsx
Reduxストアを使用してデータを表示し、更新する具体的なページの実装例。カウンターの値を増減させるボタンを提供。
import { decrement, increment } from "@/lib/features/counter/counterSlice";
import { useAppDispatch, useAppSelector } from "@/lib/hooks";
import { Container } from "@mui/material";
import { useRouter } from "next/router";
export default function ReduxPage() {
const router = useRouter();
// state` 引数はすでに `RootState` として正しく型付けされている
const count = useAppSelector((state) => state.counter.value);
const dispatch = useAppDispatch();
return (
<Container maxWidth="sm">
<div>
<button
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
Increment
</button>
<span>{count}</span>
<button
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
Decrement
</button>
</div>
<div>
<button onClick={() => router.push("/")}>TOP Page</button>
</div>
</Container>
);
}
pages/index.tsx
カウンターの現在値を表示する。これにより、Reduxを使って状態管理が行われていることを確認できる。
import React from "react";
import { Container } from "@mui/material";
import HomeNavigation from "@/components/pages/home/HomeNavigation";
import { useAppSelector } from "@/lib/hooks";
export default function Index() {
const count = useAppSelector((state) => state.counter.value);
return (
<Container maxWidth="md">
{count}
<HomeNavigation />
</Container>
);
}
この記事は役に立ちましたか?
もし参考になりましたら、下記のボタンで教えてください。
コメント