0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ReactのAction・Reducer・Context・Providerの関係を整理する

0
Posted at

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へ渡しているのか

という3点を追っていくと、かなり構造を理解しやすくなります。
image.png

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?