言語情報を Redux で管理する
環境
- Next.js
- TypeScript
- App Router
- Redux
- Redux Toolkit
準備
1. Redux Toolkit と React-Redux をインストールする | Install Redux Toolkit and React-Redux
npm install @reduxjs/toolkit react-redux
2. Redux Store を作成する | Create a Redux Store
以下が、空の Redux Store を作成した状態。
import { configureStore } from "@reduxjs/toolkit";
export const store = configureStore({
reducer: {},
});
// ↓Define Root State and Dispatch Types
// Infer the `RootState`, `AppDispatch`, and `AppStore` types from the store itself
export type RootState = ReturnType<typeof store.getState> //⭐️1.
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch //⭐️2.
export type AppStore = typeof store
- Root State と Dispatch の 型を定義する。(7行目以降)
-
You will, however, want to extract the RootState type and the Dispatch type so that they can be referenced as needed. Inferring these types from the store itself means that they correctly update as you add more state slices or modify middleware settings.
https://redux.js.org/tutorials/typescript-quick-start#define-root-state-and-dispatch-types
-
- configureStore API では、追加の型指定は必要なし。
-
Redux Toolkit's configureStore API should not need any additional typings.
https://redux.js.org/tutorials/typescript-quick-start#define-root-state-and-dispatch-types
-
- ⭐️1. ストアの全体の状態の型を定義。store.getState の戻り値の型を利用して、ストアの状態の型を取得している。これにより、コンポーネントや他の部分で状態を参照する際に型安全が保証される。
- ⭐️2. ストアの dispatch 関数の型を定義。これは、アクションをディスパッチする際に使用され、型安全なディスパッチ操作を可能にする。
3. 型付きの Hooks を定義をする | Define Typed Hooks
- 上で定義した RootState 型と AppDispatch 型を各コンポーネントにインポートすることは可能。
- しかし、アプリケーションで使用する useDispatch フックと useSelector フックの型付きバージョンを作成することが推奨されている。
- その理由は以下。
- For useSelector, it saves you the need to type (state: RootState) every time
- For useDispatch, the default Dispatch type does not know about thunks. In order to correctly dispatch thunks, you need to use the specific customized AppDispatch type from the store that includes the thunk middleware types, and use that with useDispatch. Adding a pre-typed useDispatch hook keeps you from forgetting to import AppDispatch where it's needed.
https://redux.js.org/tutorials/typescript-quick-start#define-typed-hooks
よって、以下を作成。
import { useDispatch, useSelector } from 'react-redux'
import type { AppDispatch, 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>()
-
useSelector
は、Redux ストアの状態を読み取るために使用する -
useDispatch
は、アクションをディスパッチするために使用する
これからは型ではなく変数であるため、store
設定ファイルではなく、別ファイルで定義することが重要とのこと。
Since these are actual variables, not types, it's important to define them in a separate file such as app/hooks.ts, not the store setup file. This allows you to import them into any component file that needs to use the hooks, and avoids potential circular import dependency issues.
ストアの作成
4. Redux State Slice を作成する | Create a Redux State Slice
大まかな流れは以下。
-
- Define Slice State and the initial state
-
- Create State Slice and Define Action Types
今回は、言語設定を管理したいため、以下のように記述。
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
// 1. Define Slice State and the initial state
export interface LanguageState {
language: "en" | "ja"; //⭐️1-1.
}
const initialState: LanguageState = {
language: "ja", //⭐️1-2.
};
// 2. Create State Slice and Define Action Types
export const languageSlice = createSlice({
name: "language",
initialState, //⭐️2-1.
reducers: {
setLanguage: (state, action: PayloadAction<"en" | "ja">) => { //⭐️2-2.
state.language = action.payload;
// console.log(state.language);
},
},
});
// 3.
export const { setLanguage } = languageSlice.actions;
// 4.
export default languageSlice.reducer;
-
- Define Slice State and the initial state
- 各 Sliceファイルでは、初期状態値の型を定義し、createSlice が各 Reducer の状態の型を正しく推測できるようにする必要がある。
- ⭐️1-1. Define a type for the slice state
- ⭐️1-2. Define the initial state using that type
-
- Create State Slice and Define Action Types
- ⭐️2-1.
createSlice
will infer the state type from theinitialState
argument - ⭐️2-2. 生成されたすべての Action は、Redux Toolkit の
PayloadAction<T>
型を使用して定義する必要がある。- この型は、action.payload フィールドの型を汎用引数として受け取る。
-
// Use the PayloadAction type to declare the contents of
action.payload
-
- この型は、action.payload フィールドの型を汎用引数として受け取る。
-
-
createSlice
によって自動生成されるアクションクリエーターsetLanguage
をエクスポートしている。これでコンポーネントからこのアクションを簡単にディスパッチできる。
-
-
- slice に対応し、slice の状態を更新するためのロジックを含んでいる。このリデューサーをストアの設定時に組み込むことで、対応するアクションがディスパッチされたときに状態更新が行われる。
公式ドキュメントのサンプルコード
...
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: state => {
state.value += 1
},
decrement: state => {
state.value -= 1
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
}
}
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export const selectCount = (state: RootState) => state.counter.value //📝1.
export default counterSlice.reducer
メモ
- 📝1. > Other code such as selectors can use the imported
RootState
type - 生成された Action Creater は、Reducer に指定した
PayloadAction<T>
型に基づいてペイロード引数を受け入れるように正しく型指定される。たとえば、incrementByAmount には引数として数値が必要。-
The generated action creators will be correctly typed to accept a payload argument based on the PayloadAction<T> type you provided for the reducer. For example, incrementByAmount requires a number as its argument.
https://redux.js.org/tutorials/typescript-quick-start#define-slice-state-and-action-types
-
- ⚠️
-
In some cases, TypeScript may unnecessarily tighten the type of the initial state. If that happens, you can work around it by casting the initial state using as, instead of declaring the type of the variable:
https://redux.js.org/tutorials/typescript-quick-start#define-slice-state-and-action-types
-
5. Slice Reducers をストアに追加する | Add Slice Reducers to the Store
- で作成した、Slice Reducers を ストアファイルに追加する。
import { configureStore } from "@reduxjs/toolkit";
import languageReducer from "@/features/language/languageSlice"; //⭐️ Add!
export const store = configureStore({
reducer: {
language: languageReducer, //⭐️1. Add!
},
});
...
- ⭐️1.
language
というキーに languageReducer を割り当てている。これにより、language
の状態に対するすべての操作が、 languageReducer によって処理される。
ストアの使用準備
6. Redux Store を React に反映する | Provide the Redux Store to React
6-1. 準備
- 今回は Next.js の AppRouter を使用しているため、
src/app/layout.tsx
で反映する。 - しかし、Next.js はデフォルトで Server Components 。
- 一方 Provider は、Server Components にサポートしてないので、別途 Client Component で Provider を作ってあげます。
"use client";
import { ReactNode } from "react";
import { Provider } from "react-redux";
import { store } from "@/store/store";
type StoreProviderProps = {
children: ReactNode;
};
export default function StoreProvider({
children,
}: Readonly<StoreProviderProps>) {
return (
<>
<Provider store={store}>{children}</Provider>
</>
);
}
6-2. 反映
作成したStoreProvider
コンポーネントを src/app/layout.tsx
に反映する。
...
import StoreProvider from "@/store/StoreProvider"; //⭐️
...
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<div className={layoutStyles.container}>
<StoreProvider> //⭐️
<main>{children}</main>
</StoreProvider>
</div>
</body>
</html>
);
}
メモ : react-redux の Provider を反映した場合
- 公式ドキュメントの通りに、react-redux の Provider を反映し以下のように記述すると、下記のエラーが出る。
Error: This function is not supported in React Server Components. Please only use this export in a Client Component.
...
import { store } from "@/store/store";
import { Provider } from "react-redux"; //⭐️
...
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<div className={layoutStyles.container}>
<Provider store={store}> //⭐️
<main>{children}</main>
</Provider>
</div>
</body>
</html>
);
}
ストアの使用
7. Redux State と Actions を React コンポーネントで使ってみる | Use Redux State and Actions in React Components
Redux State と Actions を使って、言語切り替えコンポーネントのベースを作成する。
"use client";
import React from "react";
// 1.
import { useAppSelector, useAppDispatch } from "@/hooks";
import { RootState, AppDispatch } from "@/store/store";
// 2.
import { setLanguage } from "./languageSlice";
export function LanguageSwitcher() {
//3.
const language = useAppSelector((state: RootState) => state.language);
//4.
const dispatch = useAppDispatch<AppDispatch>();
const langBtnClassEn = `lang_btn_en __${language === "en" ? "en" : "ja"}`;
const langBtnClassJa = `lang_btn_ja __${language === "en" ? "en" : "ja"}`;
return (
<>
<button
className={langBtnClassEn}
onClick={() => dispatch(setLanguage("en"))} //⭐️4-1.
>EN</button>
<button
className={langBtnClassJa}
onClick={() => dispatch(setLanguage("ja"))} //⭐️4-1.
>JA</button>
</>
);
}
-
- 3.で定義した、型付きの Hooks をインポートする。
- コンポーネントファイルでは、React-Redux の標準フックの代わりに、事前型付きのフックをインポートする。
-
In component files, import the pre-typed hooks instead of the standard hooks from React-Redux.
-
- 言語設定を切り替えるためのアクションクリエーターをインポートする。
-
-
useAppSelector
フックを使用して、Redux ストアから現在の言語設定 (language) を読み取る。
-
RootState
を使って、適切な型情報とともにストアの状態を参照している。
-
-
-
useAppDispatch
フックを使用して、Redux ストアの dispatch 関数を取得する。
- ⭐️4-1. ここでは、ボタンがクリックされると
setLanguage
アクションがディスパッチされ、言語設定が更新される仕組み。
-
参考
Discussion