ReactにおけるGlobal stateの管理法4選

この投稿は、弊社が提供するWESEEK TECH通信の一環です。
WESEEK TECH通信とは、WESEEKのエンジニアがキャッチアップした技術に関する情報を、techブログを通じて定期的に発信していくものです。

はじめに

今回の記事では React における Global state の管理法についてさまざまな方法を、それぞれのメリットデメリットとともに解説します。

React で大規模な開発を行う際に Global state の管理法が定まっていると非常に開発が楽になるため、是非ともこのいずれかの方法を理解しておきましょう。

Global stateの管理が必要な理由

React において、例えばコンポーネントの構成が5階層になっていた場合に、一番上の階層のコンポーネントで定義している state を一番下の階層のコンポーネントに渡したい場合はどうすれば良いでしょうか。
上から下までのそれぞれのコンポーネントで state を props として渡してあげることで、一番下の階層のコンポーネントまで渡すことができます。

しかし、この方法では state をバケツリレーのように複数のコンポーネントにまたがって渡しているため、コードが複雑化してしまいます。また、中間のコンポーネントにとっては本来必要のない props を受け取っているため、再利用の難しいコンポーネントになってしまう可能性があります。

その他にも、中間に位置するコンポーネント達は state が更新されるたびに、再レンダリングの必要がないにもかかわらず再レンダリングされてしまうという問題があります。

これらの問題を解決し、無駄なコードや無駄な挙動を減らすために Global state が必要になるということです。

1. React Context

一番初めに紹介するのは、React の標準機能として備わっている useContext を使用した方法です。
この方法は React さえ導入されていれば使用できるため、追加で他のパッケージをインストールする必要がないことがメリットです。

まず初めに、ページ全体のテーマカラーを Global state として扱うことを想定して、一番上の Top コンポーネントで定義されたテーマカラーを一番下の Bottom コンポーネントに表示させるという例を用意します。

import { useState } from 'react'

const Top = () => {
  const [themeColor] = useState('dark')

  return (
    <Middle color={themeColor} />
  )
}

const Middle = (props) => {
  return (
    <Bottom color={props.color} />
  )
}

const Bottom = (props) => {
  return (
    <div>テーマカラーは {props.color} です。</div>
  )
}

中間の Middle コンポーネントでは、次の Bottom コンポーネントに props を渡すためだけに props を受け取っていることが分かります。
(この程度であれば全然問題はないですが) もし state の渡し先がずっと下層のコンポーネントであった場合は複雑なコードとなってしまいます。
この例を元に useContext を使用して、Top コンポーネントから Bottom コンポーネントに直接 state を渡せるように改善します。

React の context 機能を使用した Global state 管理の手順は非常にシンプルで、以下の4ステップで実施できます。

  1. React.createContext で Context オブジェクトを作成する
  2. Context.Provider の value に対して state を渡す
  3. state の渡し先のコンポーネントを Context.Provider で囲う
  4. 渡し先のコンポーネントで React.useContext を使用して state を呼び出す

例を見ていきましょう。

import { useState, createContext, useContext } from 'react'

const SampleContext = createContext('light')

const Top = () => {
  const [themeColor] = useState('dark')

  return (
    <SampleContext.Provider value={themeColor}>
      <Middle />
    </SampleContext.Provider>
  )
}

const Middle = () => {
  return (
    <Bottom />
  )
}

const Bottom = () => {
  const color = useContext(SampleContext)

  return (
    <div>テーマカラーは {color} です。</div>
  )
}

まず、createContext で Context オブジェクトを作成します。
createContext の引数には Global state の初期値を設定できます。今回は light とします。

const SampleContext = createContext('light')

次に、Context.Provider の value に、Global state として管理する state を渡します。

そして、Context.Provider で Middle コンポーネントを囲います。
こうすることにより、Middle コンポーネントとその配下のコンポーネントで、useContext の使用が可能になります。

const Top = () => {
  const [themeColor] = useState('dark')

  return (
    <SampleContext.Provider value={themeColor}>
      <Middle />
    </SampleContext.Provider>
  )
}

最後に、createContext で作成した Context を useContext の引数に渡してあげることで Global state を呼び出すことができます。

const Middle = () => {
  return (
    <Bottom />
  )
}

const Bottom = () => {
  const color = useContext(SampleContext)

  return (
    <div>テーマカラーは {color} です。</div>
  )
}

これにより、中間コンポーネントがいくつ存在していようと、直接最下層のコンポーネントで state を扱えるようになりました。

SampleContext.Provider を使用しているコンポーネント以外の箇所で Global state を更新したい場合は、useState を使用し state とそれを更新するための関数の両方を Context.Provider の value に渡してあげることで更新が可能となります。

以下の例では useState を使用したカスタムフックを作成しています。

import { useState } from 'react'

const useThemeColor = () => {
  const [themeColor, setThemeColor] = useState('light')

  return {
    themeColor,
    setThemeColor
  }
}

export default useThemeColor
import { createContext, useContext } from 'react'
import useThemeColor from './hooks/useThemeColor'

const SampleContext = createContext()

const Top = () => {
  return (
    <SampleContext.Provider value={useThemeColor()}>
      <Middle />
    </SampleContext.Provider>
  )
}

const Middle = () => {
  return (
    <Bottom />
  )
}

const Bottom = () => {
  const context = useContext(SampleContext)

  return (
    // ボタンを押した時に context.themeColor を 'dark' に更新する
    <button onClick={() => context.setThemeColor('dark')}>テーマカラー更新</button>
  )
}

Global state が更新されるたびに、useContext でその Context を参照している全てのコンポーネントが再レンダリングされるため、1つの Context で全ての Global state を管理するのではなく、必要に応じて複数の Context を用意すると良いでしょう。

その他、React Context については React の公式ドキュメントに詳しく書かれているため、気になる方は是非ご覧ください。
コンテキストのガイド

この React Context を使った Global state の管理は、今回紹介する方法の中で比較的お手軽に使うことができるので、小規模なプロジェクトではこの方法を用いるのが良いのではないかと思われます。

2. Redux

Redux とは、React が扱う state の管理に特化したライブラリであり、現時点 (2022 年 4 月) における React の state 管理のデファクトスタンダードに近いポジションを取っています。

Redux - npm を見ると、Weekly Downloads が 2022 年 4 月の時点で約 780 万と非常に多く、全世界で多くの開発者に利用されていることが分かります。

Redux は特に大規模なプロジェクトに適していると言われています。Redux は Facebook が生み出した Flux というアーキテクチャに則って設計されていることが特徴です。

以下、公式ドキュメントより引用

Redux では、Store と呼ばれる場所に全ての State が保存されます。Action を Dispatch (送信) することで State を定義したり更新したりします。
Store の中の Reducer で State の更新を行います。Reducer で Dispatch された Action を受け取り、新しい State を返却します。

全体の流れを説明すると、

  1. ActionCreator が Action を作成する
  2. Store に対して Action が Dispatch(送信)される
  3. Dispatch された Action が Reducer に渡される
  4. Reducer が新しい State を生成する
  5. Store から新しい State を呼び出して画面上に表示する

となります。

Redux を導入することで、複雑な state の管理が可能になりますが、一方で学習コストが高いというデメリットが存在します。
そのため大規模なプロジェクトでの採用が適していると考えられます。

3. Recoil

Recoil とは、Facebook 社が開発し 2020 年に公開された、非常に新しい state 管理のライブラリです。

Recoil を利用した Global state の管理は実装のハードルがとても低く、React Hooks の useState のような感覚で使用できる点が大きな特徴です。

Recoil は Redux と同様に、Facebook が生み出した Flux というアーキテクチャに則って設計されており、Atom や Selector と呼ばれる単位を使用してデータを管理します。

今回もページ全体のテーマカラーを Global state として扱うことを想定して、実際に Recoil を触ってみます。

import { RecoilRoot, atom, useRecoilState } from 'recoil'

const themeColorState = atom({
  key: 'themeColorState',
  default: 'light'
})

const Top = () => {
  return (
    <RecoilRoot>
      <Middle />
    </RecoilRoot>
  )
}

const Middle = () => {
  return (
    <Bottom />
  )
}

const Bottom = () => {
  const [themeColor, setThemeColor] = useRecoilState(themeColorState)

  return (
    <div>テーマカラーは {themeColor} です。</div>
  )
}

atom は状態の一部を表しており、任意のコンポーネントから読み取りや書き込みを行うことができます。

key には他の atom と被らないような unique ID を付け、default に state の初期値を入れます。

const themeColorState = atom({
  key: 'themeColorState',
  default: 'light'
})

Recoil を使用するコンポーネントを RecoilRoot で囲みます。これにより、以下の例では Middle コンポーネントとその配下のコンポーネントで state を呼び出したり、更新したりできるようになります。

const Top = () => {
  return (
    <RecoilRoot>
      <Middle />
    </RecoilRoot>
  )
}

最後に、コンポーネント内で useRecoilState を使うことで、state の呼び出しと更新ができます。

useRecoilState から返される配列の1つ目の要素は state の現在の値であり、2 つ目の要素はそれを更新するための関数です。

const Middle = () => {
  return (
    <Bottom />
  )
}

const Bottom = () => {
  const [themeColor, setThemeColor] = useRecoilState(themeColorState)

  return (
    <div>テーマカラーは {themeColor} です。</div>
  )
}

Recoil で一番多く使うことになるであろう useRecoilState は、React Hooks の useState とほとんど同じような使い方ができるため、既に React Hooks を使いこなせている人は非常に低い学習コストで Recoil を扱うことができます。

Recoil はまだ公開されて間もなく、現時点 (2022 年 4 月) ではまだ実験的な段階ですが、今後 state 管理ライブラリの主流になるとも言われています。

Recoil を使用することにより簡単に Global state を管理できるようになるため、プロジェクトの規模によらず非常におすすめです。

4. SWR

SWR は Next.js を開発している Vercel が開発した、データ取得のための React Hooks ライブラリです。
SWR という名前は stale-while-revalidate というキャッシュ戦略から来ています。

基本的な使い方としては、useSWR という React Hooks を用いることで、API を通じたデータの取得・キャッシュを簡単に記述する手助けをしてくれます。

import useSWR from 'swr'

const fetcher = () => fetch('/api/user')

const Profile = () => {
  const { data, error } = useSWR('/api/user', fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

useSWR フックは key 文字列と fetcher 関数を受け取ります。 key はデータの一意な識別子で、fetcher に引数として渡されます。 fetcher はデータを返す任意の非同期関数で、fetch や Axios のようなツールを使うことができます。

通常はこのように、API を通じてデータを取得するために使用します。
しかし第二引数の fetcher を渡す箇所に null を渡すことにより、useSWR で Global state の管理ができるようになります。

const { data: themeColor, mutate: setThemeColor } = useSWR('themeColor', null, { initialData: 'light' });

useSWR から返ってくる data はデータの現在の値であり、mutate 関数を使用することでデータを更新できるため、React Hooks の useState のような使い方ができます。
そのため、こちらも低い学習コストで扱うことができます。

また、useSWR の第三引数には様々な option を指定でき、initialData では data の初期値を指定できます。

既にプロジェクト内でデータの取得用として SWR を導入している場合に、Global state 管理のために SWR を使用するというのが良いと思います。

まとめ

React における Global state の管理法を4つご紹介しました。
個人的には最も簡単に扱うことのできる Recoil をおすすめしますが、ライブラリを導入せずに使用できる React Context や、複雑な state 管理が可能となる Redux など、自身のプロジェクトに合った Global state の管理法を導入しましょう。