React

Redux DevToolsの使い方|React-Reduxの接続とステートの追跡

Reduxを入れる実利のうち、他の状態管理ライブラリで代替しにくいものがRedux DevToolsです。ディスパッチされたアクションが時系列で積まれ、各時点のステートを差分で比較でき、任意の時点まで画面を巻き戻せます。再現手順の長い不具合ほど、この履歴が効きます。

この記事では react-redux 9.3.0 を前提に、ReactアプリとReduxストアの接続、Redux DevToolsの導入と画面の読み方、動かないときの切り分けを扱います。Redux Toolkit そのものの構成やcreateSlice・RTK Queryの実装はRedux Toolkitとは?createSliceとRTK Queryの実装と採用判断【2026年8月版】で解説しているため、本稿では接続とデバッグに絞ります。

まとめ:接続とDevToolsで押さえる要点

  • react-redux 9.3.0 の peerDependencies は react: ^18.0 || ^19 です。React 17以前のプロジェクトでは npm 7 以降がインストール自体を ERESOLVE で止めるため、react-redux 8 系を指定します。
  • DevToolsの接続は configureStore なら設定不要です。devTools の既定値が true のため、拡張機能を入れるだけで繋がります。
  • 不具合調査で最初に開くタブはStateではなくDiffです。「関係ない値まで変わる」のか「変わるはずの値が変わらない」のかを一目で切り分けられます。
  • アクションは表示されるのにステートが空という症状は、ストア設定ではなく非シリアライズ値(MapDate、クラスインスタンス)の混入が原因です。
  • useSelector が毎回新しいオブジェクトを返すと、ストア更新のたびに再レンダリングが走ります。参照等価で比較される点が落とし穴です。

React-Reduxの接続|Provider・useSelector・useDispatch

Providerの配置とreact-redux 9のReactバージョン要件

ストアをReactツリーへ渡す役目は Provider が担います。アプリのルートを包むだけで、配下のどのコンポーネントからもストアへアクセスできるようになります。

// src/main.jsx
import { createRoot } from 'react-dom/client'
import { Provider } from 'react-redux'
import { store } from './app/store'
import App from './App'

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

ここにバージョンの制約が1つあります。react-redux 9.3.0 の peerDependencies は react: ^18.0 || ^19 で、この指定に optional は付いていません。npm 7 以降は peer 依存の衝突を警告ではなく ERESOLVE エラーとして扱うため、React 17 のプロジェクトではインストール自体が失敗します。--legacy-peer-deps で強引に入れた場合、v9 が React 18 の useSyncExternalStore を前提に shim を廃止した経緯から、実行時に破綻する点にも注意してください。公式の移行ガイドも「React-Redux v9はReact 18を必須とし、React 16や17をサポートしない」と明記しています。Reactを上げられない事情があるなら、react-redux 8 系をバージョン指定でインストールするのが唯一の解です。

useSelectorの選択範囲と再レンダリングの抑制

ストアから値を読むのが useSelector、アクションを送るのが useDispatch です。

import { useSelector, useDispatch } from 'react-redux'
import { increment } from '../features/counter/counterSlice'

function Counter() {
  const value = useSelector((state) => state.counter.value)
  const dispatch = useDispatch()
  return <button onClick={() => dispatch(increment())}>{value}</button>
}

性能上の落とし穴はセレクタの戻り値の作り方にあります。react-reduxは既定で戻り値を参照等価(===)で比較するため、useSelector((state) => ({ a: state.a, b: state.b })) のように毎回新しいオブジェクトを返す書き方では、ストアが更新されるたびに必ず再レンダリングが走ります。プリミティブ値ごとに useSelector を分けて呼ぶのが最も単純な回避策です。複数値をまとめたいなら、RTKが依存に持つreselect(RTK 2.12.0 の指定は ^5.1.0、現在の解決先は 5.2.0)でメモ化セレクタを作ります。

Reactの再レンダリング最適化そのものは、React Compilerの登場で前提が動いています。手動メモ化との線引きはReact Compilerとは?自動メモ化の仕組みと導入方法・useMemoとの違いを解説にまとめました。

useDispatchとconnectの使い分け

connectmapStateToProps は今も動作します。ただし新規コードで選ぶ理由は薄れました。フック版はコンポーネントを高階関数で包まないぶん型推論が素直で、TypeScriptとの相性が明確に良いためです。connect が実務で残る用途は、クラスコンポーネントが残存していてフックを呼べないケースに限られます。既存プロジェクトを一括で書き換える必要はありません。触るコンポーネントから順にフックへ寄せる進め方で十分です。

なお、ローカルに閉じた状態までReduxへ持ち上げる必要はありません。開閉フラグや入力中のテキストは useState、再レンダリングを起こさず値を保持したいだけなら useRef が適します。判断基準はReactのuseRefの使い方|useStateとの違い・DOM参照・値の保持を実例で解説Reactフックの基本と導入:React 16.8で追加された新機能の概要で整理しています。

Redux DevToolsの導入|拡張機能の入手とストア側の設定

拡張機能の入手先と接続確認

Redux DevToolsはブラウザ拡張として配布されています。Chromeは Chrome Web Store、FirefoxはMozilla Add-ons から入手します。Electronアプリに組み込む場合は electron-devtools-installer に REDUX_DEVTOOLS を指定してください。

導入できたかどうかは、開発サーバーを起動してブラウザの開発者ツールに「Redux」タブが増えているかで判断します。タブ自体が出ない場合は拡張がそのページに注入されていないため、インストール後にページを再読み込みします。タブは出るのに中身が空という症状は原因が別にあるので、後述の切り分け節を参照してください。

configureStoreでの既定有効とcreateStore使用時の合成

アプリ側の設定は、configureStore を使っているなら不要です。RTK公式APIドキュメントは devTools オプションについて「configureStoreがRedux DevToolsブラウザ拡張のサポートを自動的に有効にすべきかどうかを示す。既定値は true」と記載しています。古い記事にある window.__REDUX_DEVTOOLS_EXTENSION__ の手動合成は不要です。

一方、既存プロジェクトが createStore でストアを組んでいる場合は、エンハンサーを自分で合成する必要があります。createStore は Redux 4.2.0 で @deprecated マークが付きましたが、公式は「無期限に動作し続け、削除されることは決してない」「この非推奨化は純粋に視覚的な指標である」と明言しており、既存コードが動かなくなるわけではありません。打ち消し線の表示だけを消したいなら import { legacy_createStore as createStore } from 'redux' に書き換えられます。DevToolsを繋ぐ場合の合成は次の形です。

import { legacy_createStore as createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from '@redux-devtools/extension'
import { thunk } from 'redux-thunk'
import rootReducer from './reducers'

const store = createStore(
  rootReducer,
  composeWithDevTools(applyMiddleware(thunk))
)

composeWithDevTools@redux-devtools/extension パッケージの追加インストールが必要です。新規に書くなら configureStore を選び、この合成そのものを不要にするほうが早いでしょう。

Redux DevToolsの画面の読み方|Action・State・Diff・Trace

3タブの使い分けと不具合調査の入口

Reduxタブの左ペインには、ディスパッチされたアクションが発生順に積まれます。右ペインで主に使う表示は3つです。

タブ 表示内容 主な用途
Action アクションのtypeとpayload 意図した値が送られているかの確認
State そのアクション直後のストア全体 更新後の状態の検証
Diff 直前の状態からの差分のみ 意図しない箇所の書き換え検出

調査の入口はDiffです。「更新したはずの値が変わらない」なら、アクションは届いているのにリデューサーが該当ケースを処理していない可能性が高いと判断できます。「関係ない値まで変わる」なら、別スライスが同じアクションtypeに反応している疑いが濃厚です。Stateタブの全量表示から目視で探すより、差分のほうが圧倒的に速く原因へ届きます。

タイムトラベルとSkipによる原因の切り分け

アクション一覧の各行にあるJumpを押すと、その時点のステートがアプリへ再適用され、画面が過去の状態へ巻き戻ります。これがタイムトラベルです。Skipは特定のアクションだけを無かったことにして以降を再計算するため、「このディスパッチが原因か」の切り分けに使えます。

使いどころは、フォーム入力を10ステップ進めた先でしか出ない不具合のような、再現コストの高いケースです。手順を再現し直さずに、問題のアクションだけを抜いた結果を確認できます。Redux公式FAQが採用理由として挙げる「状態が時間とともにどう更新されたかを見る必要がある」は、実質この機能を指しています。

traceとmaxAgeによる履歴の調整

既定の設定では追いきれない場面が2つあります。1つは「このアクションはどこから送られたのか」がわからないケース。Traceタブにスタックトレースを出すには、trace オプションを明示的に有効にする必要があります。既定値は false で、保存されるスタックフレーム数は traceLimit の既定 10 です。

export const store = configureStore({
  reducer: rootReducer,
  devTools: {
    trace: true,
    traceLimit: 25,
    maxAge: 100,
  },
})

もう1つは、履歴が途中で切れるケースです。DevToolsが保持するアクション数は maxAge の既定 50 件で、これを超えると古いものから捨てられます。ポーリングやマウス移動でアクションが多発するアプリでは、調査したい時点の履歴が既に消えていることがあります。上記のように maxAge を引き上げてください。ただし履歴は全てメモリ上に保持されるため、極端な値は開発環境の動作を重くします。

Redux DevToolsが動かないときの切り分け

Reduxタブが無反応な場合の確認順序

Reduxタブは出るのにアクションが1件も流れてこない場合、次の順で疑うと早く終わります。

  • ストアを createStore で生成していて、DevTools用エンハンサーを合成していない(configureStore なら既定で接続済み)
  • 本番ビルドを見ている。devTools: process.env.NODE_ENV !== 'production' のように環境で切っている設定では、本番モードのビルドに接続されない
  • ストアを複数生成している。テスト用や旧実装のストアが残っていると、拡張が別インスタンスを掴む
  • Provider に渡したストアと、DevToolsへ接続したストアが別物になっている

本番環境で有効にしたままにする運用は避けてください。ストアの全内容が閲覧可能になり、認証トークンや個人情報を保持している場合は情報漏洩に直結します。

ステートが欠ける非シリアライズ値の問題

アクションは表示されるのにStateタブが空、あるいは一部のキーだけ表示されないという症状は、ストア設定ではなくステートの中身が原因です。MapSetDate オブジェクト、クラスインスタンス、関数といった直列化できない値が入ると、DevToolsはそれらを正しく表示できません。

開発中であれば、configureStore が組み込む serializableStateInvariant ミドルウェアが「A non-serializable value was detected in the state」という警告で先に教えてくれます。この警告は作法の指摘にとどまりません。直列化できない値はステートの永続化(localStorageへの保存)やSSRのハイドレーションも壊すため、値の側を直すのが本筋です。日時はISO 8601文字列かUnixタイムスタンプで持ち、Map はプレーンオブジェクトへ変換してから格納します。

ファイルアップロードで File オブジェクトを一時的にアクションへ載せるなど、どうしても通す必要がある場合は、該当のアクションtypeだけを除外します。

export const store = configureStore({
  reducer: rootReducer,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: {
        ignoredActions: ['upload/fileSelected'],
      },
    }),
})

serializableCheck: false でチェック全体を無効化する指定もありますが、以後すべての混入を見逃します。範囲を絞った除外に留めるべきです。

接続まわりで頻出するエラーの対処

ステート直接変更エラーとImmerの適用範囲

「A state mutation was detected」は、configureStore が開発時に組み込む immutableStateInvariant ミドルウェアが出すエラーです。開発時の既定ミドルウェアは actionCreatorInvariant・immutableStateInvariant・thunk・serializableStateInvariant の4種で、本番ビルドでは thunk のみが残ります。実行時チェックが本番性能に影響しない設計です。

createSlice の中では state.value += 1 と書けるのに、なぜこのエラーが出るのか。境界はImmerのドラフトが有効な範囲にあります。Immerが書き換え記法を不変更新へ変換するのは、createSlicecreateReducer のリデューサー関数の内部だけです。useSelector が返したオブジェクトへコンポーネント内で直接代入した場合や、リデューサーから受け取ったステートの一部を非同期に書き換えた場合は、ドラフトの外側での変更となりエラーになります。

もう1つの典型が、リデューサー内でドラフトを変更しつつ、同時に新しいステートを return するパターンです。この場合はImmerが「An immer producer returned a new value and modified its draft」という例外を投げます。ドラフトを書き換えるか、新しいオブジェクトを返すか、リデューサー1つにつきどちらか一方へ統一してください。

useSelectorで再レンダリングが止まらないケース

DevToolsのアクション履歴は正常なのに画面が過剰に再描画される場合、原因はストアではなくセレクタ側にあります。前述の「毎回新しいオブジェクトを返す」パターンのほか、セレクタ内で filtermap を呼んで新しい配列を生成しているケースが典型です。配列は毎回別参照になるため、中身が同じでも再レンダリングが走ります。

切り分けにはDevToolsのDiffが使えます。アクションごとのDiffが空、つまりステートが実際には変わっていないのにコンポーネントが再描画されているなら、原因はセレクタの参照同一性です。この場合はreselectでメモ化するか、必要なプリミティブ値だけを選択する形へ書き換えます。フォームのように値の更新頻度が高い状態は、そもそもReduxへ載せずReact Hook Formとは?使い方・バリデーション・v7の書き方を実例で解説のようなフォーム専用ライブラリでコンポーネント内に閉じるほうが、再レンダリングの問題を根本から避けられます。

よくある質問

Redux DevToolsに何も表示されないのはなぜですか?

最初に疑うのはストアの生成方法です。configureStore なら devTools の既定値が true のため設定不要ですが、createStore を直接使っている場合は composeWithDevTools によるエンハンサーの合成が別途必要になります。次に本番ビルドを見ていないか、テスト用など複数のストアを生成していないかを確認します。アクションは流れるのにステートが空な場合は、ステート内の非シリアライズ値(Map、クラスインスタンス等)が原因です。

createStoreで作ったストアをDevToolsに繋ぐにはどうしますか?

@redux-devtools/extension をインストールし、composeWithDevTools(applyMiddleware(...))createStore の第2引数に渡します。createStore 自体は Redux 4.2.0 で非推奨マークが付いていますが、公式が「削除されることは決してない」と明言しているため、既存コードを急いで書き換える必要はありません。打ち消し線の表示だけを消すなら legacy_createStore へのエイリアスインポートが使えます。

react-reduxはReact 17のプロジェクトで使えますか?

react-redux 9 系は使えません。9.3.0 の peerDependencies は react: ^18.0 || ^19 で、公式移行ガイドも「React-Redux v9はReact 18を必須とし、React 16や17をサポートしない」と記載しています。npm 7 以降ではインストールが ERESOLVE で失敗するため、React 17以前のままなら react-redux 8 系をバージョン指定でインストールしてください。

useSelectorを呼ぶたびに再レンダリングが起きるのはなぜですか?

セレクタが毎回新しい参照を返しているためです。react-reduxは戻り値を参照等価で比較するので、オブジェクトリテラルや filter の結果をそのまま返すと、中身が同じでも「変わった」と判定されます。プリミティブ値ごとに useSelector を分けるか、reselectでメモ化したセレクタを使ってください。

Redux DevToolsは本番環境でも有効にしてよいですか?

推奨しません。ストアの全内容が閲覧可能になるため、認証トークンやユーザーの個人情報を保持している場合は情報漏洩の経路になります。devTools: process.env.NODE_ENV !== 'production' のように環境変数で切り替えるのが一般的な運用です。

関連記事

資料請求

RELATED POSTS 関連記事