ReduxとContext APIの違い、そして何を使うべきかについて

9 min read読了の目安(約8900字

始め

Redux勉強するぞ!と思ってまずReduxが何なのかから調べました。そして「これcontext調べてた時も見たような…?」と感じました。一度整理したかったです。

いいタイトルが思い浮かばなくて以前書いたinterfaceとtypeの違い、そして何を使うべきかについてのタイトルパクりました、笑。


1. メインストレージの必要性

アプリケーション制作においてstateを管理することは大事です。Reactでstate管理と言ったらすぐuseStateが頭の中に浮かびますね。確かにuseStatestateを全部管理することもできます。

しかし、useStateで管理してるstateをたくさんのコンポーネントで使っているとしたら以下のようになります。

あるコンポーネントでstateの変更が起こり、そのstateを使ってる他のコンポーネントに変更を伝える図です。一つ確かなことはこれはめんどうくさいということです。

ここで「あ…なんか広く使われているstateは別途ストレージみたいなところで一括で管理すると楽」と思うかもしれません。それがReduxとContext APIがやってることです。

2. Redux

2-1. Reduxとは

Reduxは一言で言うとグローバルなstateを管理するためのライブラリです。そしてReact世界で一番使われているstate管理ライブラリでもあります。

npmtrendsで確認したら、ざっくり見てもReactプロジェクトの中で50-60%はreduxを採用していると推測できます。

2-2. 基本概念

Reduxが具体的にどうstateを管理しているかを簡単に説明します。概念メインですので、細かいメソッドや生成の仕方などは省略させていただきました。

Store

storeグローバルなstateが保存されるストレージです。Reduxが提供するcreateStore()という半数で生成します。

理解のために簡単な例をあげてみましょう。例えば、storeに私の銀行残高情報のstateが保存されています。

{
  userName: "みんちゃん",
  id: "abcd123",
  money: 0,
}

このstateを変更したいなら、まずactionが必要です。

Action

actionはstoreに保存されているstateに対して、何をするかが書いてあるオブジェクトです。

サンプルコードの続きです。今残高が0ですが、10¥を入金したいです。そのためにはmoneyを変更する必要がありますね。これをactionに記述したらこうなるでしょう。

{
  type: "DEPOSIT_MONEY",
  payload: 10,
}

typeactionの名前で、何をするかを書きます。今回は入金するのでDEPOSIT_MONEYにしました。金額は10です。

action createrという概念もありますが、それは単純にactionを返す関数です。関数化しといたほうが後々楽なのでやっときます。

const depositAction = (payload) => {
  return {
    type: "DEPOSIT_MONEY",
    payload,
  };
};

入金額は今後変わるかもしれませんのでpayloadという引数で動的に動くようにしました。

これで入金できる!と思いきや、できません。パソコンは人間の言語が分からねいのでDEPOSIT_MONEYだけ見てもそれが何なのか分からないでしょう。ですので、actionに対して正確に何を行うかを指定する必要があります。それがreducerです。

Reducer

reduceractiontypeを確認してそれに相応する動作を行う関数です。reduceractionを理解し、storeにあるstateを更新するのでactionstateを引数で渡されます。

actiontypeによって動作が変わるのでswitch分で書きましょう。

const reducer = (state, action) => {
  switch (action.type) {
    case "DEPOSIT_MONEY":
      return {
        ...state,
	money: action.payload,
      }
  }
};

今は入金の話だけしてますが、実際は出金や解約など色々なケースがあるので、それも全部switch文のcaseに追加すれば良いです。

これでactionの具体的な動作まで指定できましたが、まだ全部バラバラですね。actionをこのreducerまで送る必要があります。それがdispatchです。

Dispatch

dispatchactionreducerまで送ることです。

react-reduxのHooksであるuseDispatchを使ったらコンポーネント内でdispatchできる関数を返してくれるので、それを使います。

const dispatch = useDispatch();

// 入金ボタンのイベントハンドラ
const onClickDeposit = (payload) => dispatch(depositAction(payload));

見えないところでpayloadという変数に入金額が入る処理をしておいたとしましょう。
これで入金ボタンクリックしたらデータ入のactionreducerまで送られます。

🙋🏻‍♀️ React-Reduxはなんですか?

React-Reduxはstateを読み込み、storeをアップデートするためにactionをdispatchすることでReactコンポーネントがRedux storeとインタラクションできるようにする公式パッケージです。

と、Reduxの公式サイトで言ってました。要するにReactでのRedux使用をサポートするパッケージですね。ReactでReduxを使うならReact-Reduxも一緒に使うことが普通です。

要約

  • store:グローバルなstateが保存されるストレージ
  • actionstoreに保存されているstateに対して、何をするかを書いておくオブジェクト
  • action createractionを返す関数
  • reduceractiontypeを確認してそれに相応する動作を行う関数
  • dispatchactionreducerまで送ること

stateが更新される過程
actiondispatchされるとreducerstorestateを操作し、その変更がすべてのコンポーネントに反映される。

今まで話したサンプルコードの流れをビジュアルで表したらこのようになります。Reduxの公式サイトから借りてきました。

最初はなんかいっぱいあるしマジでわからなかったですが、このgifを20回ほど見てたらどんどんわかって来ました。(ちなみに「Click Event: Deposit」を送る部分がスタートで$10に変わる部分が終わりです。)

3. Context API

3-1. Context APIとは

実はReactが基本的に持ってるメインストリージもあります。それがcontextです。Reactの公式ドキュメントではcontextをこう説明しています。

コンテクストは、ある React コンポーネントのツリーに対して「グローバル」とみなすことができる、現在の認証済みユーザ・テーマ・優先言語といったデータを共有するために設計されています。

そしてこのcontextを生成してくれるAPIがContext APIというわけです。

3-2. 基本概念

それではこのContext APIの基本概念についても軽く見てみましょう。

Context

先述した通りcontextはReactが基本的に持ってるメインストリージです。createContextというメソッドを使って生成できます。

サンプルコードで見てみましょう。例えば言語を管理するcontextを作ったとします。

import { createContext } from 'react';
export const LanguageContext = createContext("kr");

このようにcreateContextで生成したcontextオブジェクトにはProviderConsumerという特別なReactコンポーネントが入ってます。

Provider

Providervalue propsを用いて子コンポーネントたちにstateを渡す役割です。Providerの中にあるすべての子コンポーネントはそのcontext内のデータにアクセスできます。

import React from 'react';
import LanguageContext from './LanguageContext'; //上で作ったcontext
import MainContent from './MainContent'; //コンポーネント
import Header from './Header'; //コンポーネント

export const App = () => {
  return (
    <LanguageContext.Provider value="kr">
      <div className="App">
        <Header />
        <MainContent />
      </div>
    </LanguageContext.Provider>
  );
}

Providerのvalue propは必須なので、書かないとこういうエラーがでます。

Warning: The value prop is required for the <Context.Provider>. Did you misspell it or forget to pass it?

これでHeaderMainContentcontextにアクセスできるようになりました。基本的にはConsumerでアクセスできます。

Consumer

ConsumerProviderから渡されたstateをもらって使う役割です。

MainContentのコードがこうだとしてみましょう。

import React from 'react';
import LanguageContext from './LanguageContext';

export const MainContent = () => {
  return (
    <LanguageContext.Consumer>
      {(language) => (
        <section language={language}>
	//何らかの内容
	</section>
      )}
    </LanguageContext.Consumer>
  );
};

Consumer内の関数でstateを使うことができます。Providervalueでkrを渡してましたから、このlanguageはkrになってるはずです。

これでも良いですが、もっと簡単に使える方法があります。useContextというhookを使う方法です。

useContext

useContextがやってることはConsumerと同じですが、Consumerで囲まなくても渡されたstateをもらって使わせてくれるhookです。

import React, { useContext } from "react";
import LanguageContext from "./LanguageContext";

export const MainContent = () => {
  const language = useContext(LanguageContext);
  return (
    <section language={language}>
      //何らかの内容
    </section>
  );
};

こちらのほうが綺麗ですね。

要約

  • context:Reactが基本的に持ってるンストリージ
  • Provider:子コンポーネントたちにstateを渡す
  • ConsumerProviderから渡されたstateをもらって使う
  • useContextConsumerで囲まなくても渡されたstateをもらって使わせてくれるhook

stateが更新される過程
Providervalue propsを更新すると、Consumer or useContextstateを使ってる子コンポーネントたちに反映される

4. ReduxとContext APIの違い

4-1. Reactの束縛

Reduxはライブラリです。Reactと一緒に使われる場合が多いですが、Reactではないフレームワーク(Vueなど)でも使えます。

Context APIはReactに搭載されている機能です。当たり前にReactではないと使えません。

4-2. メインストレージの数

Reduxのstoreは一つだけです。この一つのstoreですべてのstateを管理します。ロジックを分けたい場合は機能ごとにreducerを分けます。例えツイッターを作るとしたら「ユーザーに関するreducer」、「投稿に関するreducer」などになりそうです。

Context APIのcontextは複数作れます。ですので、機能ごとにcontextを生成することになります。ツイッターの例だったら「ユーザーに関するcontext」、「投稿に関するcontext」などでしょう。

Context APIは機能が増えたら毎回contextを生成するからめんどうだという意見もあります。

4-3. Middleware

ここが一番の違いだと思いますが、ReduxはMiddlewareが使えます。Middlewareが何かというと、こんな感じです。

Reduxの基本概念でactionreducerに送られると話しました。Middlewareを使うとこのactionreducerで処理される前に他の作業を追加することができます。例えば、

  • actionをconsoleに表示する(もしくはサーバー側でロギング)
  • 特定の条件ではactionを無視する
  • 特定のactionが発生したらjavascriptの関数を実行させる
  • 特定のactionが発生したら他のactionも発生させる

などなどです。Middlewareはよく非同期処理を扱う時に使われ、便利機能を提供してくれます。

context APIはMiddleware使えないので、このような非同期処理の機能は全部直接実装することになるのでしょう。

4-4. 複雑さ

基本概念部分で既に感じたと思いますが、Reduxは概念が多く、ディレクトリもコード量も増やしてプロジェクトを複雑にします。それにReduxを使うためにまた色々なライブラリが必要になり、ライブラリが増えたらバンドルも重くなります。この問題を解決するため、Reduxが自らredux-toolkitというライブラリを開発したぐらいです。

それに比べたらContext APIは割と簡単です。特にライブラリをインストールしなくてもProvideruseContextだけ理解してたら使えます。

5. 結論

5-1. 何を使うべきか

今までの違いを踏まえたらこのような結論に至ります。

Reduxがオススメ

  • アプリケーションの規模が大きい
  • グローバルなstate管理機能以外にも多様な機能がほしい(デバッグやロギングなど)
  • 非同期処理することが多い

Context APIがオススメ

  • アプリケーションの規模が大きくない
  • グローバルなstate管理機能だけで十分
  • 非同期処理することがあまりない

ReduxはContext APIより機能が多いですが、その分コード量もバンドルサイズも増やすことになります。メリットとデメリットをきちんと確認して採用したほうが良いでしょう。

6. その他

Mediumで「Stop Asking if React Hooks Replace Redux」という記事があります。始め方が面白いですよ。

「Do React hooks replace Redux?」という質問に対する早い答えは「いいえ(not really)」。
より微妙だけれど丁寧な答えは「まぁ, それはあなたが取り組んでいるプロジェクトの種類によりますね」。
実際に私が人々に伝えたい答えは「あなたが何を言っているのか分からない」。「Do React hooks replace Redux?」という質問が根本的に間違っている理由はいくつかある。

その理由をいくつかあげてます。気になる方は読んでみてください。


終わり

この記事…大変だった…📚でもこれでReduxを構造を理解できたので、やっとredux-sagaに進めそうです。