Next.jsのApp Routerでの開発で、グローバルステートの管理にRedux Toolkitを導入しようとしましたが、従来のやり方とは少し違ったので、公式ドキュメントを見ながら導入したので、まとめたいと思います。
1. lib/store.ts
を追加
公式ドキュメント通り追加します。
libディレクトリに配置するのが推奨されていますが、命名は任意のもので良いそうです
import { configureStore } from '@reduxjs/toolkit';
import contersSlice from './reducers/countersSlice';
export const makeStore = () => {
return configureStore({
reducer: {
// ここに使用するSliceを追加
counters: contersSlice,
}
})
}
export type AppStore = ReturnType<typeof makeStore>
export type RootState = ReturnType<AppStore['getState']>
export type AppDispatch = AppStore['dispatch']
2. lib/hooks.ts
を追加
こちらも公式ドキュメント通り追加します。
import { useDispatch, useSelector, useStore } from 'react-redux'
import type { AppDispatch, AppStore, RootState } from './store'
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
export const useAppStore = useStore.withTypes<AppStore>()
3. app/StoreProvider.tsx
を追加
こちらも公式ドキュメント通り追加。
use clientを宣言してClient Componentにします。
'use client'
import { useRef } from 'react'
import { Provider } from 'react-redux'
import { makeStore, AppStore } from '../lib/store'
export default function StoreProvider({
children
}: {
children: React.ReactNode
}) {
const storeRef = useRef<AppStore>()
if (!storeRef.current) {
storeRef.current = makeStore()
}
return <Provider store={storeRef.current}>{children}</Provider>
}
4. app/layout.tsx
を修正
ルートレイアウトでを使ってラップします
こうすることで全ルートでstoreを使用することができます
import StoreProvider from './StoreProvider';
// 省略
<StoreProvider>{children}</StoreProvider>
今までは直接ここで<Provider store={store}>
みたいな感じで書いていましたが、
AppRouterはServerComponentのため、エラーが出てしまうみたいです。
5. Sliceを追加
数字を増やしたり減らしたりするactionを追加しました。
import { createSlice } from "@reduxjs/toolkit";
export interface CounterType {
value: number;
}
const initialState: CounterType = {
value: 0,
};
const countersSlice = createSlice({
name: "counters",
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
},
});
export const { increment, decrement } = countersSlice.actions;
export const countersReducer = countersSlice.reducer;
6. 使ってみる
使用したいコンポーネントで下記のように呼び出したり、値を更新したりできます。
Client Componentでしか使用できないので注意。
'use client';
import React from 'react';
import { useAppSelector, useAppDispatch } from '@/hooks/storeHook';
import { increment, decrement } from '@/states/reducers/countersSlice';
const ExamplePage: React.FC = () => {
// stateの後のcountersは、lib/store.tsのreducerで設定したkeyです
const { value } = useAppSelector((state) => state.counters);
const dispatch = useAppDispatch();
return (
<div>
<button onClick={() => { dispatch(increment()); }}>+1</button>
<button onClick={() => { dispatch(decrement()); }}>+1</button>
</div>
);
};
export default ExamplePage;
懸念していた点
<StoreProvider>
はClient Componentなので、
それでラップすると下層は全てClient Componentになるのではないかと心配していましたが、
試しに下層コンポーネントでフックを使おうとするとエラーが出たり
console.log
を書いてみてもブラウザのコンソールには出力されませんでしたので、
問題なく下層はデフォルトのServerComponentで動作しているようでした。
詰まったところ
ページ遷移しても値が保持されているかを調べたく
<a href="/b">Bページへ</a>
として移動したところ、値が保持されていなかったので
上手く動作しない!と困惑しましたが、<Link>
を使えば解決しました。
初歩すぎて恥ずかしい、、
import Link from 'next/link';
// 省略
<Link href="/b">Bページへ</Link>
参考記事