📘

言語情報を Redux で管理する

2024/09/19に公開

環境

  • 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 を作成した状態。

app/store/store.ts
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行目以降)
  • configureStore API では、追加の型指定は必要なし。
  • ⭐️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

よって、以下を作成。

app/hooks.ts
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

大まかな流れは以下。

    1. Define Slice State and the initial state
    1. Create State Slice and Define Action Types

今回は、言語設定を管理したいため、以下のように記述。

src/features/language/languageSlice.ts
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;

    1. 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
    1. Create State Slice and Define Action Types
    • ⭐️2-1. createSlice will infer the state type from the initialState argument
    • ⭐️2-2. 生成されたすべての Action は、Redux Toolkit の PayloadAction<T> 型を使用して定義する必要がある。
      • この型は、action.payload フィールドの型を汎用引数として受け取る。
        • // Use the PayloadAction type to declare the contents of action.payload

    1. createSlice によって自動生成されるアクションクリエーターsetLanguageをエクスポートしている。これでコンポーネントからこのアクションを簡単にディスパッチできる。
    1. 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 には引数として数値が必要。
  • ⚠️

5. Slice Reducers をストアに追加する | Add Slice Reducers to the Store

  1. で作成した、Slice Reducers を ストアファイルに追加する。
app/store/store.ts
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 を作ってあげます。
src/store/StoreProvider.tsx
"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 に反映する。

src/app/[lang]/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.
src/app/[lang]/layout.tsx
...
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 を使って、言語切り替えコンポーネントのベースを作成する。

src/features/language/languageSwitcher.tsx

"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>
    </>
  );
}

    1. 3.で定義した、型付きの Hooks をインポートする。
    • コンポーネントファイルでは、React-Redux の標準フックの代わりに、事前型付きのフックをインポートする。
    • In component files, import the pre-typed hooks instead of the standard hooks from React-Redux.

    1. 言語設定を切り替えるためのアクションクリエーターをインポートする。
    1. useAppSelector フックを使用して、Redux ストアから現在の言語設定 (language) を読み取る。
    • RootState を使って、適切な型情報とともにストアの状態を参照している。
    1. useAppDispatch フックを使用して、Redux ストアの dispatch 関数を取得する。
    • ⭐️4-1. ここでは、ボタンがクリックされるとsetLanguage アクションがディスパッチされ、言語設定が更新される仕組み。

参考

https://redux.js.org/tutorials/typescript-quick-start
https://redux.js.org/tutorials/quick-start
https://zako-lab929.hatenablog.com/entry/20240308/1709899974
https://zenn.dev/nolyc/articles/9ec9f38cccb62a

Discussion