useContextとuseReducerを用いたReact状態管理の定石

Reactの状態管理

React状態管理は、Reactにおける重要なテーマです。状態管理を大きく分けると、アプリ全体での状態管理と、各コンポーネントにおけるローカルの状態管理がありますが、以下では特に断りが無い限り、アプリ全体での状態管理について説明しています。

有名なReduxは、状態管理に特化したフレームワークです。ただ、Reduxは小規模なプロジェクトにはややオーバースペックで、学習曲線の高さを考えても、本当にReduxが必要なのかをよく考慮したほうがいいでしょう。

また、useStateフックを用いた状態管理は、シンプルでわかりやすいです。ただ、コンポーネントの階層が深くなればなるほど、状態とは全く関係ない中間のコンポーネントにまで、propsをバトンリレーのように渡していく必要があります。この手法はProps Drillingと呼ばれており、コンポーネントの階層が浅くない限り、あまり推奨される手法ではありません。

ReduxuseStateを用いなくても、useContextフックとuseReducerフックを組み合わせて使用することで、状態管理が可能です。これらのフックを適切なパターン、つまり定石に従って使用することで、状態管理の処理をスマートに記述することができます。

useContextフックとuseReducerフックを平たく言うと、useContextコンポーネント間でのデータ共有useReducer状態の更新ロジックの仕組みを提供するフックです。以下で、各フックについて簡単に説明します。

useContextとは?

useContextは、コンポーネントツリー間でデータ共有の仕組みを提供するフックです。useContextを用いることにより、Props Drillingを用いなくても、データの共有が可能になります。useContextは単独で呼び出すことはなく、createContextでまずcontextを生成してから使います。

具体的には、以下のように使用します。通常は、二つのファイルに分けて処理を記述します。まずcreateContext()でContextを生成し、そしてJSXをreturnする箇所で、Context.Providerで子コンポーネントを囲みます。value属性には、子コンポーネントに渡したい状態を設定します。

// Contextの生成
import { createContext } from "react"

const Context = createContext()

return (
     <Context.Provider value={state}>
     // 子コンポーネントを配置
     </Context.Provider >
)

export default StateContext

子コンポーネントでは、useContext()を呼び出し、親コンポーネントで設定された値を取得します。

// Contextの取得
import { useContext } from "react"
import Context from "./Context"

const state = useContext(Context)

useReducerとは?

useReducerは、状態管理用フックです。useStateの上位互換という位置づけのフックです。useStateとは状態を更新する手法が異なり、dispatchという関数でアクションをreducer関数に送り、dispatchを受け取ったreducer関数で、状態を更新するロジックを実行します。

具体的には、以下のように使用します。引数には、reducer関数と、初期状態を指定します。

const initialState = {
    // 初期値の定義
}

function reducer(state, action) {
   switch (action.type) {
     // caseの定義
  }
}

const [state, dispatch] = useReducer(reducer, initialState)

サンプルコード

以下は、useContextフックとuseReducerフックを用いたサンプルコードです。以下のファイルで構成されます。

  • StateContext.js
  • DispatchContext.js
  • App.js
  • Main.js
  • Button.js

StateContext.jsDispatchContext.jsは、createContext()で生成したcontextexportしているだけのシンプルな内容です。

StateContext.js
// StateContext.js
import { createContext } from "react"

const StateContext = createContext()

export default StateContext
DispatchContext.js
// DispatchContext.js
import { createContext } from "react"

const DispatchContext = createContext()

export default DispatchContext
App.js

一番重要なAppコンポーネントです。まず、ファイルの一番上でStateContextDispatchContextimportしています。

そしてreducer関数を定義し、状態更新のロジックを記述しています。reducer関数は、子コンポーネントでアクション(ボタンのクリック等)の発生、つまりdispatch関数がコールされると呼び出されます。

reducer関数を定義したら、useReducerフックの呼び出しです。先程定義したreducer関数初期値をuseReducerに渡しています。useReducerの返り値は、statedispatchで、このstateとdispatchれを子コンポーネントでも使えるようにしているのが次の処理です。

returnのJSXを返す箇所では、StateContext.ProviderDispatchContext.Providerで子コンポーネントを囲んでいます。これでuseReducerの返り値であるstatedispatchを子コンポーネントで使用できるようになります。statedispatchを一つのProviderにまとめてもいいのですが、分割することによって、子コンポーネントにおける使い勝手とパフォーマンスを向上させています。

// App.js

import StateContext from "./StateContext"
import DispatchContext from "./DispatchContext"
import ReactDOM from "react-dom/client"
import { BrowserRouter, Routes, Route } from "react-router-dom"
import StateContext from "./StateContext"
import DispatchContext from "./DispatchContext"

function App() {
  const initialState = {
    // 初期値の定義
  }

  function reducer(state, action) {
    switch (action.type) {
      // caseの定義
    }
  }

  const [state, dispatch] = useReducer(reducer, initialState)

  return (
    <StateContext.Provider value={state}>
      <DispatchContext.Provider value={dispatch}>
        <BrowserRouter>
          <Header />
          <Routes>
            // Routeの定義
          </Routes>
          <Footer />
        </BrowserRouter>
      </DispatchContext.Provider>
    </StateContext.Provider>
  )
}

const root = ReactDOM.createRoot(document.querySelector("#app"))
root.render(<Main />)

子コンポーネントでは、StateContextDispatchContextを以下のように使います。状態を取得するにはstateContext、状態を更新する場合はdispatchContextを子コンポーネントから取得します。

以下の例は、StateContextの使用例です。

Main.js

// StateContextの使用例
import React, { useContext } from "react"
import StateContext from "../StateContext"

function Main(props) {
  const appState = useContext(StateContext)

  return (
    // appStateから値を取得して表示
  )
}

export default Main

以下の例は、DispatchContextの使用例です。ボタンだけの単純なコンポーネントです。useContextDispatchContextからdispatchを取り出し、ボタンが押された時にappDispatchを呼び出します。appDispatchという変数名にしているのは、ローカルステートとアプリ全体のステートの違いを明確にするためです。

Button.js
// DispatchContextの使用例
import React, { useContext } from "react"
import DispatchContext from "../DispatchContext"

function Main(props) {
  const appDispatch = useContext(DispatchContext)

  function handleClick() {
    appDispatch({ type: "click" })
  }

  return (
    <button onClick={handleClick}>
      Button
    </button>
  )
}

export default Main