redux の使い方

Predictable state container for JavaScript apps

v5.0.1/週MIT状態管理
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

Redux の使い方 — JavaScriptアプリのための予測可能な状態管理コンテナ

一言でいうと

Reduxは、JavaScriptアプリケーションの状態(state)を単一のストアで一元管理し、予測可能な方法で更新するための状態管理ライブラリです。Action → Reducer → Store という単方向データフローにより、アプリの挙動を一貫させ、デバッグやテストを容易にします。

重要: 現在、Redux公式は素のReduxではなく Redux Toolkit(RTK) の使用を強く推奨しています。本記事ではReduxコアのAPIを解説しますが、新規プロジェクトではRedux Toolkitの利用を検討してください。

どんな時に使う?

  1. 複数のコンポーネント間で共有する状態が多い場合 — ユーザー認証情報、カート内容、通知など、アプリ全体で参照・更新される状態を一元管理したいとき
  2. 状態の変更履歴を追跡・デバッグしたい場合 — Redux DevToolsによるタイムトラベルデバッグで、いつ・どのActionで・どう状態が変わったかを可視化できます
  3. サーバーサイドレンダリング(SSR)やテストで状態を再現したい場合 — 状態がシリアライズ可能な純粋なオブジェクトであるため、サーバーでの初期状態注入やテストでの状態再現が容易です

インストール

# Redux Toolkit(推奨)+ React-Redux
npm install @reduxjs/toolkit react-redux

# yarn
yarn add @reduxjs/toolkit react-redux

# pnpm
pnpm add @reduxjs/toolkit react-redux

# Reduxコアライブラリ単体(v5.0.1)
npm install redux

基本的な使い方

最もシンプルなカウンターの例で、Reduxコアの基本的なデータフローを示します。

import { createStore } from 'redux';
// 注意: createStore は非推奨(deprecated)マークがついていますが、
// 機能的には問題なく動作します。新規プロジェクトでは Redux Toolkit の
// configureStore を使用してください。

// 1. State の型定義
interface CounterState {
  value: number;
}

// 2. Action の型定義
type CounterAction =
  | { type: 'counter/incremented' }
  | { type: 'counter/decremented' }
  | { type: 'counter/incrementedByAmount'; payload: number };

// 3. 初期状態
const initialState: CounterState = { value: 0 };

// 4. Reducer — 純粋関数で状態を更新
function counterReducer(
  state: CounterState = initialState,
  action: CounterAction
): CounterState {
  switch (action.type) {
    case 'counter/incremented':
      return { value: state.value + 1 };
    case 'counter/decremented':
      return { value: state.value - 1 };
    case 'counter/incrementedByAmount':
      return { value: state.value + action.payload };
    default:
      return state;
  }
}

// 5. Store の作成
const store = createStore(counterReducer);

// 6. 状態の購読
const unsubscribe = store.subscribe(() => {
  console.log('Current state:', store.getState());
});

// 7. Action の dispatch
store.dispatch({ type: 'counter/incremented' });
// Current state: { value: 1 }

store.dispatch({ type: 'counter/incrementedByAmount', payload: 5 });
// Current state: { value: 6 }

store.dispatch({ type: 'counter/decremented' });
// Current state: { value: 5 }

// 8. 購読解除
unsubscribe();

よく使うReduxのAPI

1. createStore — ストアの作成

import { createStore } from 'redux';

interface AppState {
  count: number;
}

type AppAction = { type: 'INCREMENT' } | { type: 'RESET' };

const reducer = (state: AppState = { count: 0 }, action: AppAction): AppState => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'RESET':
      return { count: 0 };
    default:
      return state;
  }
};

// 第2引数でプリロード状態、第3引数でエンハンサーを渡せる
const store = createStore(reducer, { count: 10 });
console.log(store.getState()); // { count: 10 }

注意: Redux v5 では createStore に非推奨マークが付いています。公式は @reduxjs/toolkitconfigureStore を推奨しています。

2. combineReducers — 複数のReducerを合成

import { combineReducers, createStore } from 'redux';

// ユーザー関連
interface UserState {
  name: string;
  loggedIn: boolean;
}
type UserAction = { type: 'LOGIN'; payload: string } | { type: 'LOGOUT' };

const userReducer = (
  state: UserState = { name: '', loggedIn: false },
  action: UserAction
): UserState => {
  switch (action.type) {
    case 'LOGIN':
      return { name: action.payload, loggedIn: true };
    case 'LOGOUT':
      return { name: '', loggedIn: false };
    default:
      return state;
  }
};

// 通知関連
interface NotificationState {
  messages: string[];
}
type NotificationAction =
  | { type: 'ADD_NOTIFICATION'; payload: string }
  | { type: 'CLEAR_NOTIFICATIONS' };

const notificationReducer = (
  state: NotificationState = { messages: [] },
  action: NotificationAction
): NotificationState => {
  switch (action.type) {
    case 'ADD_NOTIFICATION':
      return { messages: [...state.messages, action.payload] };
    case 'CLEAR_NOTIFICATIONS':
      return { messages: [] };
    default:
      return state;
  }
};

// 合成: state.user / state.notifications としてアクセス可能
const rootReducer = combineReducers({
  user: userReducer,
  notifications: notificationReducer,
});

type RootState = ReturnType<typeof rootReducer>;

const store = createStore(rootReducer);
store.dispatch({ type: 'LOGIN', payload: 'Tanaka' } as UserAction);
console.log(store.getState().user);
// { name: 'Tanaka', loggedIn: true }

3. applyMiddleware — ミドルウェアの適用

import { createStore, applyMiddleware, Middleware } from 'redux';

// カスタムロガーミドルウェア
const loggerMiddleware: Middleware = (storeAPI) => (next) => (action) => {
  console.log('Dispatching:', action);
  const result = next(action);
  console.log('Next state:', storeAPI.getState());
  return result;
};

// 非同期処理用の簡易 thunk ミドルウェア
const thunkMiddleware: Middleware = (storeAPI) => (next) => (action) => {
  if (typeof action === 'function') {
    return action(storeAPI.dispatch, storeAPI.getState);
  }
  return next(action);
};

interface AppState {
  value: number;
}
type AppAction = { type: 'SET'; payload: number };

const reducer = (state: AppState = { value: 0 }, action: AppAction): AppState => {
  switch (action.type) {
    case 'SET':
      return { value: action.payload };
    default:
      return state;
  }
};

const store = createStore(
  reducer,
  applyMiddleware(loggerMiddleware, thunkMiddleware)
);

store.dispatch({ type: 'SET', payload: 42 });
// Dispatching: { type: 'SET', payload: 42 }
// Next state: { value: 42 }

4. compose — 関数合成ユーティリティ

import { createStore, applyMiddleware, compose } from 'redux';

// Redux DevTools Extension との連携でよく使われるパターン
const composeEnhancers =
  (typeof window !== 'undefined' &&
    (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||
  compose;

interface AppState {
  count: number;
}
type AppAction = { type: 'INC' };

const reducer = (state: AppState = { count: 0 }, action: AppAction): AppState => {
  switch (action.type) {
    case 'INC':
      return { count: state.count + 1 };
    default:
      return state;
  }
};

const loggerMiddleware = (storeAPI: any) => (next: any) => (action: any) => {
  console.log('Action:', action.type);
  return next(action);
};

const store = createStore(
  reducer,
  composeEnhancers(applyMiddleware(loggerMiddleware))
);

5. bindActionCreators — Action Creatorをdispatchにバインド

import { createStore, bindActionCreators } from 'redux';

// Action Creators
const increment = () => ({ type: 'INCREMENT' as const });
const decrement = () => ({ type: 'DECREMENT' as const });
const addAmount = (amount: number) => ({
  type: 'ADD_AMOUNT' as const,
  payload: amount,
});

type AppAction = ReturnType<typeof increment | typeof decrement | typeof addAmount>;

interface AppState {
  count: number;
}

const reducer = (state: AppState = { count: 0 }, action: AppAction): AppState => {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'ADD_AMOUNT':
      return { count: state.count + action.payload };
    default:
      return state;
  }
};

const store = createStore(reducer);

// dispatch を自動的にバインド
const actions = bindActionCreators(
  { increment, decrement, addAmount },
  store.dispatch
);

// dispatch を明示的に呼ばなくてよい
actions.increment();       // store.dispatch(increment()) と同等
actions.addAmount(10);     // store.dispatch(addAmount(10)) と同等
console.log(store.getState()); // { count: 11 }

類似パッケージとの比較

特徴Redux (コア)Redux Toolkit (RTK)ZustandMobXJotai
バンドルサイズ~2kB~11kB~1kB~16kB~3kB
ボイラープレート多い少ない非常に少ない少ない非常に少ない
学習コスト中〜高
DevTools✅(拡張機能)✅(組み込み)✅(Redux DevTools互換)✅(限定的)
ミドルウェア✅(thunk組み込み)❌(不要)❌(不要)
TypeScript対応✅(手動型定義が多い)✅(型推論が優秀)
設計思想Flux / 関数型Flux / 関数型Flux風 / シンプルリアクティブ / OOPアトミック
React依存なしなしなしなしあり

結論: 新規プロジェクトでReduxを選ぶなら Redux Toolkit 一択です。よりシンプルな状態管理で十分なら ZustandJotai も有力な選択肢です。

注意点・Tips

1. createStore の非推奨について

Redux v5 で createStore に視覚的な非推奨マーク(取り消し線)が付きましたが、削除される予定はありません。これは「Redux Toolkitの configureStore を使ってほしい」というシグナルです。

// 非推奨マークを消したい場合は legacy_createStore を使う
import { legacy_createStore as createStore } from 'redux';

2. Reducerでは必ず新しいオブジェクトを返す

// ❌ NG: 直接ミューテーション
const badReducer = (state: AppState, action: any) => {
  state.count += 1; // 参照が変わらないので再レンダリングされない
  return state;
};

// ✅ OK: 新しいオブジェクトを返す
const goodReducer = (state: AppState, action: any) => {
  return { ...state, count: state.count + 1 };
};

3. Redux v5 の破壊的変更に注意

Redux v5.0 では以下の変更があります(v4からの移行時に注意):

  • TypeScript 4.7以上が必須
  • Action の type プロパティが string 型に限定symbol は不可に)
  • AnyAction が非推奨 → UnknownAction に変更
  • Middleware の型パラメータが変更されています

4. ストアは原則1つ

// ❌ アンチパターン: 複数ストア
const userStore = createStore(userReducer);
const productStore = createStore(productReducer);

// ✅ 推奨: combineReducers で1つにまとめる
const rootReducer = combineReducers({
  user: userReducer,
  product: productReducer,
});
const store = createStore(rootReducer);

5. Redux DevTools は開

比較記事