useContextとuseReducerを用いたReact状態管理の定石
Reactの状態管理
Reactの状態管理は、Reactにおける重要なテーマです。状態管理を大きく分けると、アプリ全体での状態管理と、各コンポーネントにおけるローカルの状態管理がありますが、以下では特に断りが無い限り、アプリ全体での状態管理について説明しています。
有名なReduxは、状態管理に特化したフレームワークです。ただ、Reduxは小規模なプロジェクトにはややオーバースペックで、学習曲線の高さを考えても、本当にReduxが必要なのかをよく考慮したほうがいいでしょう。
また、useStateフックを用いた状態管理は、シンプルでわかりやすいです。ただ、コンポーネントの階層が深くなればなるほど、状態とは全く関係ない中間のコンポーネントにまで、propsをバトンリレーのように渡していく必要があります。この手法はProps Drillingと呼ばれており、コンポーネントの階層が浅くない限り、あまり推奨される手法ではありません。
ReduxやuseStateを用いなくても、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.jsとDispatchContext.jsは、createContext()で生成したcontextをexportしているだけのシンプルな内容です。
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コンポーネントです。まず、ファイルの一番上でStateContextとDispatchContextをimportしています。
そしてreducer関数を定義し、状態更新のロジックを記述しています。reducer関数は、子コンポーネントでアクション(ボタンのクリック等)の発生、つまりdispatch関数がコールされると呼び出されます。
reducer関数を定義したら、useReducerフックの呼び出しです。先程定義したreducer関数と初期値をuseReducerに渡しています。useReducerの返り値は、stateとdispatchで、このstateとdispatchれを子コンポーネントでも使えるようにしているのが次の処理です。
returnのJSXを返す箇所では、StateContext.ProviderとDispatchContext.Providerで子コンポーネントを囲んでいます。これでuseReducerの返り値であるstateとdispatchを子コンポーネントで使用できるようになります。stateとdispatchを一つの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 />)
子コンポーネントでは、StateContextとDispatchContextを以下のように使います。状態を取得するには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の使用例です。ボタンだけの単純なコンポーネントです。useContextでDispatchContextから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