最近、Reduxを使った既存コードを触る機会が多くありました。
久しぶりにReduxのコードを読むと、
-
storeとは何だったか -
stateとreducerの関係は何だったか -
actionとdispatchは何をしているのか - 画面操作から
state更新まで、どう流れているのか
といった各メソッドや概念の役割が、少し曖昧になっていました。
そこで今回は、Redux公式ドキュメントの説明を確認しながら、Reduxの基本を改めて整理します。
まずはReduxの基本的な考え方を思い出すことを目的にします。
Reduxとは
Redux公式の Getting Started with Redux では、Reduxは次のように説明されています。
Redux is a JS library for predictable and maintainable global state management.
日本語にすると、Reduxは「予測可能で保守しやすいグローバル状態管理のためのJavaScriptライブラリ」です。
つまりReduxは、アプリケーション内で共有する状態を、決められたルールで管理するための仕組みです。
Reactだけでも、コンポーネント内の状態は useState で管理できます。
const [keyword, setKeyword] = useState('');
この keyword が1つのコンポーネント内だけで使われるなら、useState で十分です。
しかし、アプリケーションが大きくなると、複数の画面やコンポーネントで同じ状態を使いたい場面が出てきます。
たとえば、次のような状態です。
- ログイン中のユーザー情報
- 検索条件
- マスターデータ
- ダイアログの表示状態
- 通知メッセージ
- APIから取得した共通データ
こうした状態を各コンポーネントでバラバラに管理すると、どこで値が変わったのか追いづらくなります。
Reduxでは、共有したい状態を store という場所にまとめ、action と reducer というルールを通して更新します。
Reduxが解決したいこと
Redux公式の Redux Fundamentals, Part 2: Concepts and Data Flow では、Reactの小さなカウンターを例に、アプリケーションには大きく次の3つがあると説明されています。
| 用語 | 役割 |
|---|---|
state |
アプリを動かす信頼できる状態 |
view |
現在の state をもとにしたUI |
actions |
ユーザー操作などによって発生し、state 更新のきっかけになる出来事 |
小さなコンポーネントであれば、useState だけでも十分です。
function Counter() {
const [counter, setCounter] = useState(0);
return (
<button onClick={() => setCounter(counter + 1)}>
{counter}
</button>
);
}
この例では、state も更新処理もコンポーネント内に閉じています。
しかし、複数のコンポーネントで同じ state を共有したくなると、話が少し複雑になります。
親コンポーネントに state を持ち上げる方法もありますが、コンポーネントの階層が深くなったり、離れた場所にあるコンポーネント同士で同じ state を使いたくなったりすると、propsの受け渡しがつらくなります。
Reduxの基本的な考え方は、共有したい state をコンポーネントツリーの外に置き、どのコンポーネントからでも参照・更新できるようにすることです。
ただし、自由にどこからでも書き換えるのではなく、決められた流れで更新します。
store・state・reducerの関係
Reduxを理解するときに、まず押さえたいのが store、state、reducer の関係です。
ざっくり言うと、次のようになります。
| 用語 | 役割 |
|---|---|
store |
state を保存している箱 |
state |
現在のデータ |
reducer |
state を更新するためのルール |
Redux公式の Getting Started with Redux では、アプリ全体のグローバル state は、単一の store 内にobject treeとして保持されると説明されています。
つまり、store はアプリケーション全体の状態を持つ場所です。
たとえば検索フォームの状態を考えると、state は次のようになります。
type SearchFormState = {
keyword: string;
isOpen: boolean;
};
この state は、現在の検索フォームの状態を表しています。
const initialState: SearchFormState = {
keyword: '',
isOpen: false,
};
一方、reducer はこの state をどう更新するかを決める関数です。
function searchFormReducer(state = initialState, action) {
switch (action.type) {
case 'searchForm/changeKeyword':
return {
...state,
keyword: action.payload,
};
default:
return state;
}
}
reducer は、現在の state と action を受け取り、新しい state を返します。
reducer(currentState, action) => newState
ここで大事なのは、state が reducer に何かを送っているわけではないということです。
Reduxの store が、現在の state とdispatchされた action を reducer に渡します。
そして reducer が返した新しい state を、store が保存します。
流れとしては次のようになります。
storeが現在のstateを持っている
↓
actionがdispatchされる
↓
storeがreducerに currentState と action を渡す
↓
reducerが newState を返す
↓
storeが newState を保存する
💡 ポイント
storeは「状態を持つ場所」、stateは「現在の中身」、reducerは「中身をどう変えるかのルール」と考えると分かりやすいです。
actionとは
Redux公式の Getting Started with Redux では、state を変更する唯一の方法は、何が起きたかを表す action を作り、それを store へ dispatch することだと説明されています。
action は、アプリケーション内で起きた出来事を表すオブジェクトです。
たとえば、検索キーワードが変更された場合は次のような action になります。
{
type: 'searchForm/changeKeyword',
payload: 'React'
}
この action は、次の出来事を表しています。
検索キーワードがReactに変更された
type は、何が起きたのかを表す名前です。
payload は、その action に必要な追加データです。
この例では、変更後のキーワードである 'React' が payload に入っています。
action は「stateをどう変更するか」ではなく、「何が起きたか」を表すものです。
実際に state をどう変更するかは、後述する reducer が担当します。
dispatchとは
dispatch は、action を store へ送る処理です。
dispatch({
type: 'searchForm/changeKeyword',
payload: 'React',
});
Reactの onClick や onChange は、ブラウザ/React側のイベントです。
そのイベントをきっかけに、Reduxへ action を dispatch します。
<input
value={keyword}
onChange={(event) => {
dispatch({
type: 'searchForm/changeKeyword',
payload: event.target.value,
});
}}
/>
つまり、Reduxでは画面から state を直接変更するのではなく、次の流れで state を更新します。
画面操作
↓
actionをdispatch
↓
reducerがstateを更新
💡 ポイント
onChangeはReact側のイベント、actionはReduxに渡す「アプリ内で何が起きたか」の情報です。
reducerとは
reducer は、現在の state と action を受け取り、新しい state を返す関数です。
Redux公式の Getting Started with Redux では、reducer の関数シグネチャは次のように説明されています。
(state, action) => newState
たとえば、検索キーワードを更新する reducer は次のようになります。
const initialState = {
keyword: '',
isOpen: false,
};
function searchFormReducer(state = initialState, action) {
switch (action.type) {
case 'searchForm/changeKeyword':
return {
...state,
keyword: action.payload,
};
case 'searchForm/open':
return {
...state,
isOpen: true,
};
default:
return state;
}
}
この reducer は、次のようなルールを持っています。
-
searchForm/changeKeywordが来たらkeywordを更新する -
searchForm/openが来たらisOpenをtrueにする - それ以外なら現在の
stateをそのまま返す
Reduxでは、state を直接書き換えるのではなく、新しい state を返すのが基本です。
return {
...state,
keyword: action.payload,
};
このように、既存の state をコピーしつつ、変更したい値だけを差し替えます。
💡 ポイント
Reduxの基本では、
stateを直接変更せず、新しいstateを返すことが重要です。
Reduxの一方向データフロー
Reduxの基本的な流れは、一方向です。
Redux公式の Redux Fundamentals, Part 2: Concepts and Data Flow では、state が特定時点のアプリ状態を表し、UIはその state に基づいて描画され、ユーザー操作などが起きると state が更新され、新しい state に基づいてUIが再描画されると説明されています。
図にすると、次のような流れです。
state
↓
view
↓
action
↓
reducer
↓
new state
↓
view
もう少しReactの画面操作に寄せると、次のようになります。
storeにstateがある
↓
Reactがstateをもとに画面を表示する
↓
ユーザーが入力・クリックする
↓
actionをdispatchする
↓
reducerが新しいstateを返す
↓
storeのstateが更新される
↓
Reactが再描画される
Reduxの良いところは、state 更新の流れが一本道になっていることです。
どこかで突然値が変わるのではなく、action を dispatch し、reducer を通して state が更新されます。
そのため、既存コードを読むときも、次の順番で追うことができます。
- どの
actionがdispatchされたのか - どの
reducerが処理したのか - どの
stateが変わったのか
selectorとは
selector は、store の state から必要な値を取り出す関数です。
たとえば、検索キーワードを取り出す selector は次のようになります。
const selectKeyword = (state: RootState) =>
state.searchForm.keyword;
Reactコンポーネントでは、React Reduxの useSelector を使って値を取得できます。
const keyword = useSelector(selectKeyword);
React Redux公式の Hooks では、useSelector はRedux storeの state から値を取り出すためのフック、useDispatch は dispatch 関数を取得するためのフックとして説明されています。
useDispatch を使うと、コンポーネント内から action を dispatch できます。
const dispatch = useDispatch();
dispatch({
type: 'searchForm/changeKeyword',
payload: 'React',
});
selector を見ると、そのコンポーネントがRedux storeのどの値に依存しているか分かります。
既存コードを読むときは、useSelector や mapStateToProps を探すと、画面がどの state を参照しているかを追いやすくなります。
Redux Toolkitについて少しだけ触れる
Reduxの基本概念を理解するうえでは、action、reducer、store の関係を押さえることが大事です。
ただし、現在のReduxではRedux Toolkitを使う書き方が公式に推奨されています。
Redux公式の Getting Started with Redux では、Redux ToolkitはReduxロジックを書くための公式推奨アプローチと説明されています。
Redux Toolkitを使うと、configureStore や createSlice を使って、store設定やreducer/actionの定義を簡潔に書けます。
Redux Toolkit公式の createSlice は、slice名、初期 state、reducer関数群を受け取り、action creatorとaction typeを自動生成するAPIです。
const searchFormSlice = createSlice({
name: 'searchForm',
initialState: {
keyword: '',
},
reducers: {
changeKeyword(state, action) {
state.keyword = action.payload;
},
},
});
このコードでは一見 state を直接変更しているように見えます。
state.keyword = action.payload;
しかしRedux Toolkitでは内部でImmerが使われており、実際にはimmutable updateとして処理されます。
この記事では、Redux Toolkitの細かい使い方には入りません。
まずは、Reduxの基本的な流れを理解することを優先します。
💡 ポイント
現在Reduxを新しく書く場合は、Redux Toolkitを使うのが公式推奨です。
ただし、既存コードを読むためには、
action・dispatch・reducer・storeの基本概念を理解しておくことが大切です。
既存コードを読むときの順番
Reduxの既存コードを読むときは、次の順番で見ると理解しやすいです。
-
storeにどのreducerが登録されているかを見る - 対象機能の
stateの形を見る -
useSelector/mapStateToPropsで参照している値を見る -
dispatchしている箇所を見る -
actionのtypeやpayloadを見る -
reducerでstateがどう更新されるかを見る
いきなりすべてのファイルを理解しようとすると大変です。
まずは1つの入力項目やボタンを選び、次の観点で追うとReduxの流れが見えやすくなります。
- この値はどこから来ているのか
- どの操作で更新されるのか
- どの
reducerで処理されるのか
Reduxは最初に全体を見ようとすると少し構えてしまいますが、1つの値を追いかけると急に読みやすくなります。
迷子になったら、まず dispatch を探す。Redux散歩の基本です。
まとめ
今回は、Redux公式ドキュメントを確認しながら、Reduxの基本を整理しました。
Reduxは、アプリケーション全体で共有する状態を store に置き、action を dispatch し、reducer で新しい state を作る仕組みです。
重要なのは、state を直接変更するのではなく、次の流れで更新することです。
dispatch(action)
↓
reducer(currentState, action)
↓
newState
↓
storeに保存
↓
画面が再描画される
Reduxの基本用語を整理すると、次のようになります。
| 用語 | 役割 |
|---|---|
store |
state を保存している場所 |
state |
現在のデータ |
action |
何が起きたかを表すオブジェクト |
dispatch |
action を store へ送る処理 |
reducer |
現在の state と action から新しい state を作る関数 |
selector |
state から必要な値を取り出す関数 |
Reduxのコードを読むときは、まずこの流れを頭に置いておくと理解しやすくなります。
次回は、action creatorや非同期処理を見ながら、既存プロジェクトでReduxのコードをどう追っていくかを整理します。