ReactとReduxを組み合わせたState管理の基本と実践をわかりやすく解説

皆さんは、React を使ったアプリでデータ管理に戸惑った経験はないでしょうか。 小さなアプリなら単純なコンポーネント間のデータ受け渡しでも問題はありません。 しかし少し規模が大きくなると「どこで状態を管理すべきか」「どのタイミングで更新すべきか」といった課題が生まれるかもしれませんね。

こうした問題を解決するために、Redux が考案されました。 Reactと組み合わせることで、アプリの状態を一元管理できるようになります。 本記事ではReact Reduxの導入方法からメリット、具体的なコード例まで丁寧に説明していきます。 初めてReduxを学ぶ皆さんでも理解できるよう、実務でどう役立つのかを意識しながら進めていきましょう。

ReactとReduxの基本的な役割

ReactはUIを構築するためのライブラリで、コンポーネント単位で画面を組み立てます。 一方でReduxはアプリ全体のState を一括で管理する仕組みを提供します。 この組み合わせによって、コンポーネントがデータをやり取りする際の複雑さを軽減できるのではないでしょうか。

Reduxの中心的な概念

Reduxは、以下の3つの要素を中心に動きます。

Store

アプリケーション全体のStateを保持します。 1つのStoreにアプリで使うStateを集約し、どこからでも同じデータを参照しやすくします。

Action

Stateの変更を「こう変えたい」という形で表すオブジェクトです。 ユーザーの操作やバックエンドからのレスポンスなど、何かイベントが起こるたびにActionが発行されます。

Reducer

Actionの内容を確認し、現在のStateをどのように更新するかを決めます。 1つのStateとActionから、新しいStateを返すのがReducerの役割です。

この3つを理解することで、ReduxがState管理をわかりやすく整理している仕組みが見えてきます。 React Reduxは、このReduxをReactのコンポーネントから使いやすいようにするためのライブラリですね。

React Reduxを導入すると得られるメリット

Reactのコンポーネント同士のデータのやり取りを単純にpropsで行うと、コンポーネント階層が深くなるにつれ煩雑になることがあります。 コンポーネント同士の結合度合いが高まり、「どこで何が更新されているのか」追跡しづらくなるケースもあるでしょう。

ですが、React Redux を導入すれば、以下のようなメリットが期待できます。

データの流れが明確

すべてのStateはStoreで一元管理されます。 どのコンポーネントが状態を更新するかがはっきりしやすくなるんですね。

バグの発見がしやすい

Reducerを追っていけばStateがどのように変化しているかがわかります。 不要な再レンダリングの原因も特定しやすくなります。

拡張性

新しい機能を追加しても、Reduxのアーキテクチャに沿って実装すればコンポーネント同士が複雑に絡まりません。 大規模な開発や長期的な保守にも向いています。

一方で、Reduxを用いるとコードの量が増える傾向があるかもしれません。 しかし、それを上回るメリットが実務では重要視されることも多いですね。

実務での活用シーン

アプリケーションがある程度の規模になると、状態を多く扱う場面が増えてきます。 例えば、ユーザー認証情報や複雑なフィルタリング設定、データの一覧取得などが該当します。

チーム開発の場合

複数の開発者が関わるとき、コードの可読性や一貫性が欠かせません。 ReduxならAction、Reducer、Storeという形でコードの構造が明確に分かれるので、初めて参加したメンバーでも流れを把握しやすいですね。

長期運用のプロジェクトの場合

時間の経過とともに機能が追加されることを考慮すると、Stateの扱いがシンプルなまま維持される仕組みは魅力的です。 Reduxの設計パターンに従えば、将来的に大きくなったプロジェクトでも、状態管理が破綻しにくいのではないでしょうか。

React Reduxの基本的な使い方

ここからは具体的なセットアップやコード例を通じて、React Reduxの使い方を見ていきます。 まずは小さなアプリをイメージして、計算やカウントを扱う例を考えてみましょう。

Redux Toolkitでの導入

以前はReduxの導入にいくつか手順がありましたが、今は Redux Toolkit という公式のライブラリが推奨されています。 下記のようにしてRedux Toolkitをインストールし、簡単にStoreを作成できます。

npm install @reduxjs/toolkit react-redux

インストール後は、ReactアプリのどこかでStoreを作成します。 以下は store.js というファイルを想定した例です。

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

// スライスの定義
const counterSlice = createSlice({
  name: "counter",
  initialState: {
    value: 0
  },
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    reset: (state) => {
      state.value = 0;
    }
  }
});

// スライスからアクションとReducerを取得
export const { increment, decrement, reset } = counterSlice.actions;

// Redux ToolkitによるStore作成
export const store = configureStore({
  reducer: {
    counter: counterSlice.reducer
  }
});

createSlice ではStateの初期値やアクションに対応する処理をまとめて定義しています。 configureStore によってStoreを一箇所で管理できるのがわかりやすいですね。

Reactコンポーネントとの連携

ReactのコンポーネントからReduxのStateを参照したり、Actionを発行したりするには、ProviderHooks を使います。 react-redux が提供する Provider コンポーネントでラップし、useSelectoruseDispatch フックを使って操作します。

ルートコンポーネントへのProvider設定

まずはアプリ全体を Provider で包むイメージです。 通常は index.jsApp.js の最上位付近に配置します。

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { Provider } from "react-redux";
import { store } from "./store";

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

こうすることで、App 以下のコンポーネントはReduxのStoreにアクセスできるようになります。

コンポーネント内でのState参照と更新

先ほどのカウンターの例を使ってみましょう。

import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement, reset } from "./store";

function Counter() {
  // 現在のcount値を取得
  const count = useSelector((state) => state.counter.value);

  // Actionをディスパッチするためのフック
  const dispatch = useDispatch();

  return (
    <div>
      <p>現在のカウント: {count}</p>
      <button onClick={() => dispatch(increment())}>増やす</button>
      <button onClick={() => dispatch(decrement())}>減らす</button>
      <button onClick={() => dispatch(reset())}>リセット</button>
    </div>
  );
}

export default Counter;

useSelector でStore内の counter.value を取り出せます。 ボタンをクリックすると increment()decrement() がActionとして発行され、Reducerを介してStateが更新される仕組みです。 こうした流れが単純化されているのがReact Reduxの強みですね。

実務的に使うためのポイント

実際の現場では、一度に扱うStateがカウンターのように単純ではないことがほとんどです。 複数の機能に対応するためにStoreを分割したり、非同期の処理を組み込む場合があります。

Storeの分割

大規模なアプリでは、例えば ユーザー管理製品カタログ管理 など、まったく性質が異なるStateを扱うことがあるのではないでしょうか。 その場合はReduxの combineReducers を使ったり、Redux Toolkitで複数のSliceを作成して、それぞれをまとめることを考えます。

export const store = configureStore({
  reducer: {
    counter: counterSlice.reducer,
    user: userSlice.reducer,
    products: productsSlice.reducer
  }
});

上記のようにすると、state.counterstate.user といった具合に分けて管理ができます。

非同期処理

外部APIとのやりとりなど、非同期のデータ取得が必須のケースも多いでしょう。 Redux Toolkitには createAsyncThunk という機能があり、これを利用すると非同期の処理をわかりやすく管理できます。

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";

// 非同期処理の定義
export const fetchUserData = createAsyncThunk(
  "user/fetchUserData",
  async (userId) => {
    const response = await axios.get(`/api/users/${userId}`);
    return response.data;
  }
);

// userSliceの定義
const userSlice = createSlice({
  name: "user",
  initialState: {
    data: null,
    status: "idle",
    error: null
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserData.pending, (state) => {
        state.status = "loading";
      })
      .addCase(fetchUserData.fulfilled, (state, action) => {
        state.status = "succeeded";
        state.data = action.payload;
      })
      .addCase(fetchUserData.rejected, (state, action) => {
        state.status = "failed";
        state.error = action.error.message;
      });
  }
});

fetchUserData という関数を使い、ユーザー情報を取得する例です。 非同期処理の進捗状況やエラーハンドリングもSliceの中で一括管理できるので、コードの見通しを保ちやすいのではないでしょうか。

Reduxを使う際に気をつけたい点

Reduxは便利ですが、導入するうえでいくつか注意点もあります。 プロジェクトの性質や規模に応じて、本当にReduxが必要かを考えることが大切です。

React Contextとの比較

Reactには Context API という仕組みもあります。 小~中規模程度であれば、Contextだけでデータの共有をシンプルに実装できる可能性があります。 一方で、Reduxにはツールチェーンの充実やミドルウェアによる拡張性といった長所があるため、プロジェクトの規模や将来的なメンテナンスを想定して選ぶと良いですね。

ActionやReducerが増えすぎる

必要な機能が増えるにしたがって、ActionやReducerの数も増えてしまうかもしれません。 しかしRedux Toolkitを使えばSliceごとに定義をまとめやすいので、過去のReduxよりは負担が減っていると言えるでしょう。

Reduxを導入するか否かは、チームの規模やアプリの複雑さで判断するのが無難です。

よくある質問への回答

初心者の皆さんがReact Reduxを調べるとき、次のような疑問を感じることがあるのではないでしょうか。 代表的な質問をいくつかピックアップしてみます。

Reduxなしでやってはいけないのか

絶対にReduxが必要というわけではありません。 むしろ、小さなアプリにはReduxは重たいと感じるかもしれません。 しかし状態管理が複雑化するようなら、導入を検討する価値は十分あります。

非同期処理はどう書くのがベストか

先ほど触れたRedux Toolkitの createAsyncThunk を使うのが手軽です。 それ以外にも redux-thunkredux-saga などのミドルウェアが存在しますが、最近は公式推奨のRedux Toolkitにまとめるのが一般的ですね。

Hooksを使わない昔の書き方とどちらが良いのか

React Reduxには、以前は connect 関数を使う方法が主流でした。 今はHooksが公式ドキュメントでも推奨されているので、特に理由がなければHooks中心で書くのがわかりやすいです。

実務イメージ:簡易TODOアプリ

最後に、React Reduxを利用した簡単なTODOアプリのイメージを示します。 新しいタスクを追加し、一覧表示と完了状態の管理を一元化してみましょう。

スライス定義

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

const todoSlice = createSlice({
  name: "todo",
  initialState: {
    tasks: []
  },
  reducers: {
    addTask: (state, action) => {
      state.tasks.push({
        id: Date.now(),
        text: action.payload,
        completed: false
      });
    },
    toggleComplete: (state, action) => {
      const task = state.tasks.find((t) => t.id === action.payload);
      if (task) {
        task.completed = !task.completed;
      }
    }
  }
});

export const { addTask, toggleComplete } = todoSlice.actions;

export default todoSlice.reducer;

TODOアプリ用のReducerを todoSlice にまとめました。 タスクを追加する addTask と、完了状態を切り替える toggleComplete を定義しています。

Store登録

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

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

ここでtodoSlice.reducerを1つのStoreに登録します。 このStoreをアプリ全体で使えるように Provider で包みます。

Reactコンポーネント

import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { addTask, toggleComplete } from "./todoSlice";

function TodoApp() {
  const tasks = useSelector((state) => state.todo.tasks);
  const dispatch = useDispatch();
  const [inputValue, setInputValue] = useState("");

  const handleAddTask = () => {
    if (inputValue.trim()) {
      dispatch(addTask(inputValue));
      setInputValue("");
    }
  };

  return (
    <div>
      <h2>TODOアプリ</h2>
      <input
        type="text"
        placeholder="タスクを入力"
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <button onClick={handleAddTask}>追加</button>

      <ul>
        {tasks.map((task) => (
          <li
            key={task.id}
            style={{
              textDecoration: task.completed ? "line-through" : "none"
            }}
            onClick={() => dispatch(toggleComplete(task.id))}
          >
            {task.text}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default TodoApp;

これで、タスクの追加や完了状態の切り替えを1つのStoreで管理できます。 複数のコンポーネントでタスクを表示するときも、同じStoreからStateを参照できるため混乱が少なくなります。

まとめ

React Reduxを活用すれば、アプリ全体のState管理をわかりやすく整理できる可能性が高いです。 コンポーネント同士のデータ受け渡しに苦労しているなら、一度Reduxの導入を検討してみると良いでしょう。 とはいえ、小さなアプリにはオーバーヘッドが大きいかもしれません。

実務では、拡張性や保守性を重視するあまり複雑になりすぎないようにバランスをとることも大切ですね。 Redux Toolkitを使えば以前より導入しやすく、コードの可読性も向上しやすいです。

React Reduxは、初心者でも着実に理解すれば扱いやすい選択肢となるのではないでしょうか。 チーム開発や中・大規模プロジェクトで求められる安定したState管理を実現するために、ぜひ基本の概念や実装手順を押さえてみてください。

Reactをマスターしよう

この記事で学んだReactの知識をさらに伸ばしませんか?
Udemyには、現場ですぐ使えるスキルを身につけられる実践的な講座が揃っています。