Reactを触っていると、次のような言葉がまとめて登場することがあります。
- State
- Action
- Reducer
- dispatch
- useReducer
- Context
- Provider
それぞれ単体では分かっても、
ActionとReducerはどうつながっているのか
ContextとProviderは何が違うのか
ReducerとContextはなぜ一緒に使われることが多いのか
あたりが、最初は少し分かりにくいところです。
この記事では、それぞれの役割と関係をできるだけシンプルに整理します。
まず全体像
最初に全体の流れを見ると、理解しやすくなります。
ユーザー操作
↓
dispatch(Action)
↓
Reducer
↓
新しいState
↓
Provider
↓
Context
↓
各コンポーネント
↓
画面が再描画される
一言ずつ表すと、次のようになります。
| 要素 | 役割 |
|---|---|
| State | 現在の状態 |
| Action | 「何をしたいか」を表す情報 |
| Reducer | Stateをどう変更するか決める |
| dispatch | ActionをReducerへ送る |
| useReducer | Reducerを使ってStateを管理するHook |
| Context | 値を複数コンポーネントで共有する仕組み |
| Provider | Contextに値を渡し、配下へ提供する |
Stateとは
Stateは、単純にいうと
「今どうなっているか」
を表すデータです。
例えばカウンターなら、
type State = {
count: number;
};
というStateを持つことができます。
現在、
{
count: 3
}
なら、
現在のカウントは3
という状態です。
Actionとは
Actionは、
「何をしたいか」をReducerへ伝えるためのデータ
です。
例えば、
type Action =
| { type: 'INCREMENT' }
| { type: 'DECREMENT' };
と定義したとします。
これは、
INCREMENT
→ 1増やしたい
DECREMENT
→ 1減らしたい
という意味です。
ポイントは、
Action自身はStateを変更しない
ということです。
Actionはあくまで、
「こういう操作が発生しました」
という通知です。
Reducerとは
Reducerは、
現在のStateとActionを受け取って、次のStateを作る関数
です。
基本形は次のようになります。
function reducer(state, action) {
return newState;
}
例えば、
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1,
};
case 'DECREMENT':
return {
...state,
count: state.count - 1,
};
default:
return state;
}
}
現在、
{
count: 3
}
というStateに対して、
{
type: 'INCREMENT'
}
というActionが来ると、
Reducerは、
{
count: 4
}
という新しいStateを返します。
つまり、
State
+
Action
↓
Reducer
↓
新しいState
という関係です。
dispatchとは
dispatchは、
ActionをReducerへ送るための関数
です。
例えば、
dispatch({
type: 'INCREMENT',
});
とすると、
Reducerへ
{
type: 'INCREMENT'
}
というActionが送られます。
流れとしては、
ボタンを押す
↓
dispatch({
type: 'INCREMENT'
})
↓
Reducer
↓
count + 1
となります。
useReducerとは
ReactでReducerを利用するためのHookが、
useReducer
です。
例えば、
const [state, dispatch] = useReducer(reducer, {
count: 0,
});
と書きます。
ここで、
state
には現在の状態が入り、
dispatch
を使ってActionを送ることができます。
例えば、
<button
onClick={() => {
dispatch({
type: 'INCREMENT',
});
}}
>
+1
</button>
とすると、
ボタン押下
↓
dispatch
↓
Action
↓
Reducer
↓
State更新
↓
再描画
となります。
Contextとは
ここまでの話は、1つのコンポーネント内でも完結します。
しかし実際のアプリでは、
Header
Sidebar
Main
Modal
Toolbar
など、離れた複数のコンポーネントから同じStateを利用したいことがあります。
そこで使われるのが、
Context
です。
Contextは、
複数のコンポーネントで値を共有するための仕組み
です。
例えば、
const CounterContext = createContext(null);
のようにContextを作成します。
Providerとは
Contextを作っただけでは、まだ値は入っていません。
そこで登場するのが、
Provider
です。
Providerは、
Contextに実際の値を設定して、配下のコンポーネントへ提供する
役割を持ちます。
例えば、
<CounterContext.Provider
value={{
count: state.count,
dispatch,
}}
>
{children}
</CounterContext.Provider>
とします。
すると、このProviderの配下にあるコンポーネントから、
const context = useContext(CounterContext);
として値を取得できます。
ContextとProviderの違い
ここは少し分かりにくいところですが、
Contextは、
「共有するための仕組み」
Providerは、
「実際に値を流すもの」
と考えると分かりやすいです。
例えば、
Context
= 配管
Provider
= 配管に水を流す側
Component
= 水を使う側
というイメージです。
ReducerとContextは別の役割
ReducerとContextは一緒に使われることが多いですが、目的は別です。
Reducerは、
Stateをどう更新するか
を担当します。
Contextは、
Stateをどこからでも使えるようにする
ことを担当します。
つまり、
Reducer
= 状態更新
Context
= 状態共有
です。
そのため、
useReducer
だけ使うこともできます。
逆に、
useState
で管理している値をContextで共有することもできます。
必ずしも、
Context + useReducer
がセットというわけではありません。
なぜContextとuseReducerを組み合わせるのか
アプリが大きくなると、
画面A
画面B
サイドバー
ダイアログ
ツールバー
など複数の場所から、
選択する
追加する
削除する
更新する
確認する
といった操作を行うことがあります。
このような場合、
useReducer
→ 状態更新をまとめる
Context
→ 状態を各コンポーネントへ共有する
という役割分担が便利になります。
典型的な構成は次のようになります。
Provider
│
├─ useReducer
│ │
│ ├─ State
│ ├─ Reducer
│ └─ dispatch
│
└─ Context.Provider
│
├─ State
└─ 操作関数
↓
子コンポーネント
dispatchをそのまま公開しない場合もある
実際のアプリでは、
dispatch({
type: 'INCREMENT',
});
を各コンポーネントから直接呼ばせず、
操作用の関数を用意することもあります。
例えば、
const increment = () => {
dispatch({
type: 'INCREMENT',
});
};
とします。
Providerでは、
<CounterContext.Provider
value={{
count: state.count,
increment,
}}
>
{children}
</CounterContext.Provider>
とします。
利用側は、
increment();
だけで済みます。
すると、コンポーネント側は、
どんなActionをdispatchするか
を知らなくてもよくなります。
全体としては、
Component
↓
操作関数
↓
dispatch
↓
Action
↓
Reducer
↓
State
↓
Provider
↓
Context
↓
Component
という流れになります。
ActionとReducerを分けるメリット
例えば、
setState(...)
をいろいろな場所で直接書くこともできます。
をいろいろな場所で直接書くこともできます。
しかし状態更新が複雑になると、
この操作ではAを変更
この操作ではBも変更
削除時にはCも変更
Undo時には元に戻す
といった処理が増えていきます。
Reducerを使うと、
どんな操作が存在するか
→ Action
その操作でStateをどう変更するか
→ Reducer
という形で整理できます。
そのため、
複雑なState管理ほどReducerが有効
になってきます。
Reducerで大切なこと
Reducerでは、基本的に現在のStateを直接書き換えません。
例えば、
state.count++;
ではなく、
return {
...state,
count: state.count + 1,
};
のように、
新しいStateを返す
という形にします。
Reducerは、
現在のState
+
Action
↓
新しいState
を返す関数、と覚えておくと理解しやすいです。
最後に整理
それぞれを一言で整理すると、
State
= 今どうなっているか
Action
= 何をしたいか
Reducer
= どう変更するか
dispatch
= Actionを送る
useReducer
= Reducerを使ってStateを管理する
Context
= 何を共有するか
Provider
= どこまで共有するか
です。
そして全体の流れは、
ユーザー操作
↓
操作関数
↓
dispatch
↓
Action
↓
Reducer
↓
State更新
↓
Provider
↓
Context
↓
コンポーネント
↓
画面更新
となります。
Reactのコードを読むときも、
このActionは何を表しているのか
このReducerでは何を更新しているのか
このProviderは何をContextへ渡しているのか
