12k
All articles

Reactにおけるreduxの理解:プロのようにステート管理を行う

Reduxはデータを一つのストアに集約し、Reactのステート管理を簡素化する。アクション、リデューサー、Redux Toolkitの連携による予測可能な更新の仕組みを解説する。

OpenReplay Team
OpenReplay Team
Reactにおけるreduxの理解:プロのようにステート管理を行う

Reactアプリケーションで複数のコンポーネント間のステート管理に苦労していますか?アプリが大きくなるにつれて、複数層のコンポーネントを通じてpropsを渡す方法(prop drilling)は扱いにくく、エラーが発生しやすくなります。Reduxは、Reactアプリケーションをより予測可能かつデバッグしやすくする集中型ステート管理システムを提供することで、解決策を提供します。

この包括的なガイドでは、ReduxがReactでどのように機能するか、いつ使用すべきか、そしてプロジェクトで効果的に実装する方法を学びます。コアコンセプトから、Reduxコードを書く現代的な方法であるRedux Toolkitを使った実践的な実装まで、すべてをカバーします。

重要なポイント

  • Reduxは、Reactアプリケーションの集中型ステート管理を提供します
  • 多くのコンポーネント間で複雑な共有ステートがある場合にReduxを使用します
  • Reduxは一方向のデータフローに従います:アクション → リデューサー → ストア → UI
  • Redux Toolkitはボイラープレートコードを減らすことでRedux開発を簡素化します
  • 大規模なReduxアプリケーションではパフォーマンス最適化が重要です

ReactにおけるReduxとは?

Reduxは、JavaScriptアプリケーション、特にReactで人気のある予測可能なステートコンテナです。アプリケーションの全ステートを「ストア」と呼ばれる単一の不変オブジェクトに保存します。このステート管理の集中型アプローチは、Reactアプリケーションの多くの一般的な問題を解決します:

  • Prop drilling:複数のコンポーネント層を通じてステートを渡す必要がなくなります
  • ステート同期:コンポーネント間で一貫したステートを確保します
  • 予測可能な更新:ステート変更は厳格な一方向データフローに従います
  • デバッグ:ステート変更を追跡可能で予測可能にします

ReduxはReact自体の一部ではありませんが、React Reduxを通じて補完的なライブラリとして機能します。これはReact向けの公式ReduxUIバインディングライブラリです。

// Basic Redux integration with React
import { Provider } from 'react-redux';
import store from './store';

function App() {
  return (
    <Provider store={store}>
      <YourApplication />
    </Provider>
  );
}

いつReduxを使うべきか?

すべてのReactアプリケーションがReduxを必要とするわけではありません。以下の場合にReduxの使用を検討してください:

  • アプリに多くのコンポーネント間で共有される複雑なステートロジックがある
  • コンポーネントがコンポーネントツリーの異なる部分からステートにアクセスして更新する必要がある
  • アプリケーション全体でステート変更を追跡してデバッグする必要がある
  • 複数の開発者が関わる中〜大規模のコードベースを持つアプリケーション

小規模なアプリケーションやローカライズされたステートを持つコンポーネントの場合、Reactの組み込みステート管理(useState、useReducer、Context API)で十分なことが多いです。

Reduxのコアコンセプト

ストア:単一の信頼できる情報源

Reduxストアはアプリケーションの全ステートツリーを保持します。コンポーネント間に分散しているReactのコンポーネントステートとは異なり、Reduxはすべてのステートを一箇所に集中させます:

// Creating a Redux store
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';

const store = configureStore({
  reducer: rootReducer
});

export default store;

アクション:ステート変更の記述

アクションはアプリケーションで何が起こったかを記述するプレーンなJavaScriptオブジェクトです。これらはReduxストアにデータを送信する唯一の方法です:

// An action object
{
  type: 'counter/incremented',
  payload: 1
}

// Action creator function
const increment = (amount) => {
  return {
    type: 'counter/incremented',
    payload: amount
  }
}

すべてのアクションには、どのような種類のアクションであるかを説明するtypeプロパティが必要です。payloadプロパティには、アクションに必要なデータが含まれます。

リデューサー:ステート更新のための純粋関数

リデューサーは現在のステートとアクションを受け取り、新しいステートを返す純粋関数です。アクションに応じてアプリケーションのステートがどのように変化するかを指定します:

// A simple reducer function
const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case 'counter/incremented':
      return state + action.payload;
    case 'counter/decremented':
      return state - action.payload;
    default:
      return state;
  }
}

リデューサーは以下の条件を満たす必要があります:

  • 純粋関数であること(副作用なし)
  • ステートを直接変更しないこと
  • 変更が発生した場合は新しいステートオブジェクトを返すこと

一方向のデータフロー

Reduxは厳格な一方向データフローに従います:

  1. コンポーネントからアクションをディスパッチする
  2. Reduxストアはアクションをリデューサーに渡す
  3. リデューサーはアクションに基づいて新しいステートを作成する
  4. ストアはステートを更新し、接続されたすべてのコンポーネントに通知する
  5. コンポーネントは新しいステートで再レンダリングする

この一方向のフローにより、ステート変更が予測可能で理解しやすくなります。

React Reduxの統合

Provider:ReduxとReactの接続

ProviderコンポーネントはReduxストアをアプリケーションのすべてのコンポーネントで利用可能にします:

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

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

フック:コンポーネントでのReduxへのアクセス

React Reduxは、コンポーネントがReduxストアと対話できるようにするフックを提供します:

useSelector:ステートの読み取り

import { useSelector } from 'react-redux';

function CounterDisplay() {
  // Select the counter value from the store
  const count = useSelector(state => state.counter.value);
  
  return <div>Current count: {count}</div>;
}

useSelectorフックは:

  • ストアステートからデータを抽出するセレクタ関数を受け取ります
  • 選択されたステートが変更されたときにコンポーネントを再レンダリングします
  • 不要な再レンダリングを避けることでパフォーマンスを最適化します

useDispatch:ステートの更新

import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';

function Counter() {
  const count = useSelector(state => state.counter.value);
  const dispatch = useDispatch();

  return (
    <div>
      <div>Count: {count}</div>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  );
}

useDispatchフックは:

  • ストアのディスパッチ関数を返します
  • 任意のコンポーネントからアクションをディスパッチできるようにします

Redux ToolkitによるモダンなRedux

Redux ToolkitはReduxロジックを書くための公式で推奨される方法です。以下の方法でRedux開発を簡素化します:

  • ボイラープレートコードの削減
  • 一般的なパターンに役立つユーティリティの提供
  • ストアセットアップの良いデフォルト設定の提供
  • リデューサーでの直接的なステート変更の使用を可能にする(Immerを通じて)

configureStoreによるストアの作成

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';
import todosReducer from './features/todos/todosSlice';

const store = configureStore({
  reducer: {
    counter: counterReducer,
    todos: todosReducer
  }
});

export default store;

createSliceによるステートロジックの定義

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

const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: (state) => {
      // Redux Toolkit allows us to write ""mutating"" logic in reducers
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    }
  }
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

createAsyncThunkによる非同期操作の処理

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

// Create an async thunk for fetching data
export const fetchUserData = createAsyncThunk(
  'users/fetchUserData',
  async (userId, thunkAPI) => {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    return await response.json();
  }
);

const userSlice = createSlice({
  name: 'user',
  initialState: {
    data: null,
    status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
    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;
      });
  }
});

export default userSlice.reducer;

実践的な実装例

Redux Toolkitを使用して簡単なカウンターアプリケーションを構築しましょう:

1. ストアのセットアップ

// store.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';

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

2. カウンター機能用のスライスを作成

// features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    }
  }
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;

// Selector
export const selectCount = (state) => state.counter.value;

3. ストアをReactに接続

// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { store } from './store';
import App from './App';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

4. コンポーネントでReduxを使用

// features/counter/Counter.js
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
  increment,
  decrement,
  incrementByAmount,
  selectCount
} from './counterSlice';

export function Counter() {
  const count = useSelector(selectCount);
  const dispatch = useDispatch();
  const [incrementAmount, setIncrementAmount] = useState('2');

  return (
    <div>
      <div>
        <button onClick={() => dispatch(decrement())}>-</button>
        <span>{count}</span>
        <button onClick={() => dispatch(increment())}>+</button>
      </div>
      <div>
        <input
          value={incrementAmount}
          onChange={(e) => setIncrementAmount(e.target.value)}
        />
        <button
          onClick={() => 
            dispatch(incrementByAmount(Number(incrementAmount) || 0))
          }
        >
          Add Amount
        </button>
      </div>
    </div>
  );
}

パフォーマンス最適化テクニック

Reselectによるメモ化セレクタ

Reselect(Redux Toolkitに含まれる)を使用すると、入力が変更された場合にのみ結果を再計算するメモ化されたセレクタ関数を作成できます:

import { createSelector } from '@reduxjs/toolkit';

// Basic selectors
const selectItems = state => state.items;
const selectFilter = state => state.filter;

// Memoized selector
export const selectFilteredItems = createSelector(
  [selectItems, selectFilter],
  (items, filter) => {
    // This calculation only runs when items or filter changes
    return items.filter(item => item.includes(filter));
  }
);

不要な再レンダリングの回避

ステートの無関係な部分が変更されたときにコンポーネントが再レンダリングされるのを防ぐには:

  1. コンポーネントが必要とする特定のデータのみを選択する
  2. 派生データにはメモ化されたセレクタを使用する
  3. オブジェクトを選択する場合は、useSelectorの第二引数としてshallowEqual関数を使用する
import { useSelector, shallowEqual } from 'react-redux';

// This component will only re-render when user.name or user.email changes
function UserInfo() {
  const { name, email } = useSelector(state => ({
    name: state.user.name,
    email: state.user.email
  }), shallowEqual);
  
  return (
    <div>
      <h2>{name}</h2>
      <p>{email}</p>
    </div>
  );
}

Reduxと他のステート管理ソリューションの比較

ReduxとContext API

Context API:

  • Reactに組み込まれている
  • 小〜中規模のアプリケーションにはよりシンプル
  • 追加のライブラリは不要
  • ボイラープレートが少ない
  • デバッグツールが限られている

Redux:

  • 複雑なステートロジックにより強力
  • 頻繁な更新に対するパフォーマンスが向上
  • Redux DevToolsによる優れたデバッグ
  • 副作用のためのミドルウェアサポート
  • ステート管理へのより構造化されたアプローチ

ReduxとZustand/Recoil/MobX

以下の場合、これらの代替案を検討してください:

  • よりシンプルなAPIとボイラープレートの削減が必要な場合(Zustand)
  • アトムベースのステート管理が必要な場合(Recoil)
  • よりオブジェクト指向的でリアクティブなアプローチを好む場合(MobX)

以下が必要な場合は、Reduxが最適な選択肢です:

  • 成熟した、実績のあるソリューション
  • 優れたデバッグ機能
  • ミドルウェアと拡張機能の大きなエコシステム
  • 複雑なアプリケーションのための予測可能なステート管理

結論

Reduxは、Reactアプリケーションのための強力で予測可能なステート管理ソリューションを提供します。いくつかの複雑さとボイラープレートを導入しますが、Redux Toolkitは開発体験を大幅に簡素化します。多くのコンポーネント間で共有されるステートを持つ複雑なアプリケーションでは、Reduxは保守性、デバッグ、予測可能性の面で明確なメリットを提供します。

ステート管理ソリューションを選択する際には、アプリケーションのニーズを慎重に検討してください。より単純なアプリケーションでは、Reactの組み込みステート管理で十分かもしれませんが、アプリケーションの複雑さが増すにつれて、Reduxの構造化されたアプローチがますます価値を持つようになります。

よくある質問

ReduxとReact Contextはどのように使い分けるべきですか?

複雑なアプリケーションや頻繁な更新と共有ステートがある場合はReduxを使用してください。より単純なアプリケーションや、あまり変更されない値を共有する必要がある場合はContextを使用してください。

Reduxは学習が難しいですか?

Reduxには理解に時間がかかるコアコンセプトがありますが、Redux Toolkitは学習曲線を大幅に簡素化します。小さな例から始めて、徐々に複雑さを増していきましょう。

Reduxはアプリを遅くしますか?

必ずしもそうではありません。Reduxはいくつかのオーバーヘッドを追加しますが、パフォーマンス最適化を含んでおり、不要な再レンダリングを防ぐことで実際にパフォーマンスを向上させることができます。

Reduxで非同期操作をどのように処理しますか?

Redux Thunk(Redux Toolkitに含まれる)やより複雑な非同期フローのためのRedux Sagaなどのミドルウェアを使用します。Redux ToolkitのcreateAsyncThunkは非同期操作を簡単にします。

ReactフックがあるのにReduxはまだ関連性がありますか?

はい。useReducerやuseContextのようなフックはより単純なステート管理のニーズを処理できますが、Reduxは特に大規模なチーム間で複雑なアプリケーションステートを管理するのに優れています。

Reduxアプリケーションをどのようにデバッグしますか?

Redux DevTools Extensionを使用して、ステート、アクション、時間の経過に伴うステート変更を検査します。これはReduxの最も強力な機能の1つです。

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers

We use cookies to improve your experience. By using our site, you accept cookies.