coded Kazuya Imoto - Front-End Developer site.

Reduxで状態管理してみる!

公開日:2025年1月25日

はじめに

これまでContext APIやZustandを使って状態の一元管理を行った経験はありますが、Reduxは利用したことがなかったため、今回試してみます。

状態管理とは?

状態管理とは、データ(状態)をコンポーネント内またはコンポーネント間で更新するプロセスのことを指します。

Reactでは標準で利用できるContext APIを使ってグローバルステートが使えます。

ローカルステートとグローバルステート

状態管理には、ローカルステートとグローバルステートの2種類があります。

  1. ローカルステート:コンポーネント内のみで状態管理ができるものです。他コンポーネントには影響しません。
    状態管理のライブラリを導入していない場合は、useStateを使用します。
  2. グローバルステート:コンポーネント間で使用できる状態管理のこと。グローバルステートを使用することで、propsのバケツリレーが不要になります。
    状態管理のライブラリを導入していない場合でも、useContextを使用することでグローバルステートの管理が可能です。

状態管理のライブラリは何がある?

主な状態管理ライブラリには、以下のものがあります。

  1. Redux
  2. Recoil
  3. Zustand
  4. Jotai
  5. Valtio

npm trendsで比較するとReduxが圧倒的に採用率が高いようです。

ライブラリの選定はプロダクトにより最適なものが異なると思いますが
今回はReduxを使用するため、ReduxとContext APIの使用シーンを比較してみました。

状態管理

適した使用シーン

Context API

  • 小規模アプリケーション向け
  • 状態が単純な場合(テーマの切り替え、認証情報の共有など)
  • 非同期処理があまりない
  • 学習コスト:低

Redux

  • 中~大規模アプリケーション向け
  • 状態の依存関係が複雑、または管理する状態が多い場合
  • デバッグツールや非同期処理のサポートが重要な場合
  • 学習コスト:高

Reduxの状態管理を使ってTODOアプリを作ってみる

Reduxに適したシーンは概ね理解できたので、ReactとReduxを組み合わせてアプリケーションを作成しながら学んでいきます。

1. 下準備

環境を準備するため、以下の手順でReactとTypeScriptの環境を構築します。

$ npm create vite@latest
> npx
> create-viteProject name: ... redux-todoSelect a framework: » ReactSelect a variant: » TypeScript

次に、開発環境が正常に起動するか確認します。
pnpmがインストールされていない場合は、npmまたはyarnを使用してインストールと動作確認を行ってください。

$ pnpm i
...
$ pnpm dev

次に、デフォルトで記載されているsrc/App.tsxsrc/main.tsxを以下のように書き換えます。

// src/App.tsx

function App() {
  return (
    <div>
      <h1>Todoリスト</h1>
    </div>
  );
}

export default App;
// src/main.tsx

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <App />
  </StrictMode>
);

次に、Redux 関連のライブラリをインストールします。
Reduxのハンズオン系の記事では、reduxとreact-reduxをインストールするような記述を見かけるかと思いますが、後述するStoreの作成時に使用するcreateStoreは現在非推奨となっています。
公式の推奨方法である @reduxjs/toolkit をインストールして対応します。

$ pnpm i @reduxjs/toolkit react-redux

以上で、下準備は完了です!

2. Storeを作成

srcディレクトリ直下にstoreディレクトリを作成し、index.tsファイルを作成します。
index.ts内は以下を追加します。

// src/store/index.ts

import { configureStore } from "@reduxjs/toolkit";

export const store = configureStore({
  reducer: {},
});

Storeとは?

⇒ Reduxの中心的な仕組みで、アプリケーション全体の状態(state)を一言管理すつためのオブジェクトです。
Reduxを使用する際には、このstoreを通じて状態を読み取ったり、変更を行ったりします。

3. Providerコンポーネントの設定

storeからすべてのコンポーネントからアクセスできるように src/main.tsx を更新します。

// src/main.tsx

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import { Provider } from "react-redux";
import { store } from "./store/index.ts";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </StrictMode>
);

ここではreact-reduxからインポートしたProviderでAppコンポーネントをラップしています。
さらに作成したstoreをpropsとしてProviderに渡しています。

4. Sliceファイルを追加

storeディレクトリ内にtodoSliceファイルを作成します。

// src/store/todoSlice.tsx

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

interface Todo {
  name: string;
  complete: boolean;
}

interface TodoState {
  lists: Todo[];
}

const initialState: TodoState = {
  lists: [
    { name: "朝食を作る!", complete: false },
    { name: "昼食を作る!", complete: false },
  ],
};

export const todoSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {},
});

export default todoSlice.reducer;

createSliceとは?
⇒ Redux Toolkit の機能のひとつで、Reduxの状態管理を簡潔に扱うためのツールです。従来のReduxでは、アクションやリデューサーを個別に定義していましたが、createSlice を使うことで、それらを一箇所でまとめて記述できます。

ここでは「TODOリスト」の状態と、それを操作するための機能を定義しています。

  1. name: todo ⇒ このスライスの名前を定義しています。Reduxで生成されるアクションのタイプやデバッグ時にこの名前が利用されます。
  2. initialState 初期状態を定義しています。ここでは初期状態として「朝食を作る!」と「昼食を作る!」というTODOタスクが設定しています。
  3. reducers: {}状態を更新するための関数(リデューサー)を定義するためのプロパティです。
  4. export default todoSlice.reducer; ⇒ 最後にtodoSlice.reducerをエクスポートしています。スライスを作成すると、createSlice によってリデューサーが自動生成されます。このリデューサーが、状態を変更する際に実行される関数として動作します。

5. store/index.tsにtodoReducerを追加

todoSliceを作成したら、store/index.tsにtodoSliceを追加します。

// src/store/index.ts

import { configureStore } from "@reduxjs/toolkit";
import todoReducer from "./todoSlice";

export const store = configureStore({
  reducer: {
    todos: todoReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;

リデューサーをインポートする場合、import todoReducer from "./todoSlice"; という点に気をつけてください。

ついでに呼び出した際の状態全体の型の定義を追加しています。

6. AppへTodoリストを呼び出す

storeのindex.tsにtodoSliceを追加したら src/App.tsx にTodoリストを表示させます。

// src/App.tsx

import { useSelector } from "react-redux";
import { RootState } from "./store";

function App() {
  const lists = useSelector((state: RootState) => state.todos.lists);

  return (
    <div>
      <h1>Todoリスト</h1>
      <ul>
        {lists.map((list, index) => (
          <li key={index}>{list.name}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;
  • useSelectorとは?
    ⇒ Redux ストアから状態(state)のデータを取得するために使用します。このフックを利用することでコンポーネントから特定の状態を簡単に参照できます。

以下のように初期状態のリストが表示されていれば成功です!

7. リスト理由

取得したlistsに対してfilter関数を実行して未完了と完了に分けます。

// src/App.tsx

import { useSelector } from "react-redux";
import { RootState } from "./store";

function App() {
  const lists = useSelector((state: RootState) => state.todos.lists);

  return (
    <div>
      <h1>Todoリスト</h1>
      <h2>未完了</h2>
      <ul>
        {lists
          .filter((list) => list.complete === false)
          .map((list, index) => (
            <li key={index}>{list.name}</li>
          ))}
      </ul>
      <h2>完了</h2>
      <ul>
        {lists
          .filter((list) => list.complete === true)
          .map((list, index) => (
            <li key={index}>{list.name}</li>
          ))}
      </ul>
    </div>
  );
}

export default App;

8. リストの移動

最後に未完了から完了へのリスト移動をできるようにします。

まずはtodoSliceのreducersを追加します。

// src/store/todoSlice.tsx

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

interface Todo {
  name: string;
  complete: boolean;
}

interface TodoState {
  lists: Todo[];
}

const initialState: TodoState = {
  lists: [
    { name: "朝食を作る!", complete: false },
    { name: "昼食を作る!", complete: false },
  ],
};

export const todoSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {
    doneList: (state, action) => {
      const { name } = action.payload;
      const item = state.lists.find((item) => item.name === name);
      if (item) {
        item.complete = true;
      }
    },
  },
});

export const { doneList } = todoSlice.actions;

export default todoSlice.reducer;

次にAppに完了ボタンを追加します。

// src/App.tsx

import { useDispatch, useSelector } from "react-redux";
import { RootState } from "./store";
import { doneList } from "./store/todoSlice";

function App() {
  const lists = useSelector((state: RootState) => state.todos.lists);
  const dispatch = useDispatch();

  return (
    <div>
      <h1>Todoリスト</h1>
      <h2>未完了</h2>
      <ul>
        {lists
          .filter((list) => list.complete === false)
          .map((list, index) => (
            <li key={index}>
              {list.name}
              <button onClick={() => dispatch(doneList({ name: list.name }))}>
                完了
              </button>
            </li>
          ))}
      </ul>
      <h2>完了</h2>
      <ul>
        {lists
          .filter((list) => list.complete === true)
          .map((list, index) => (
            <li key={index}>{list.name}</li>
          ))}
      </ul>
    </div>
  );
}

export default App;

完成

以下のように「完了」ボタンクリック後、完了へ移動すれば完成です!

今回はReduxの状態をキャッチアップするために、未完了⇒完了への移動のみでしたが必要に応じてTodo追加、削除などもチャレンジしてみてください!

さいごに

React習得当初は、「Reduxは学習コストが高い」と感じてなかなか手をつけられずにいましたが、基本的な部分は意外とスムーズに理解できたかなと思います。

とはいえ、将来的に(関わる機会があるかはわかりませんが)、中~大規模のアプリにReduxを導入したプロジェクトへ参画することになれば、壁にぶつかる予感がプンプンします。なので今後も定期的にキャッチアップしておきたいと思います...笑

参考

Redux Fundamentals, Part 1: Redux Overview | Redux

【React Redux初心者向け】Todoリスト作成を通してしっかり学ぶRedux | アールエフェクト