2023-05-11 投稿

ReactのContext Hooksを使って状態管理を実現する(createContext, useContext)

Reactの状態管理のためのパッケージとしてはReduxやRecoilなどがありますが、そのようなパッケージを使わなくてもReactのContext(Hooks、フック)を作って状態管理を実現することができます。

Context Hooks作成の流れ

  1. 扱いたい値やメソッドをInterfaceで定義する
  2. createContextでContextを作成する
  3. 作成したContextのプロバイダーを定義する
  4. useContextでフックを定義する

扱いたい値やメソッドをInterfaceで定義する

TypeScript利用の場合、 状態管理したいデータのInterfaceを定義しておきます。

1// スコアの定義
2interface Score {
3  teamA: number,
4  teamB: number
5}
6// チームAとチームBのスコア記録したい
7interface ScoreBoardContextType {
8  score: Score
9  addPoint: (team: 'teamA'|'teamB', point: number) => void
10  getResult: () => string
11}

AチームとBチームがいるようなスコアボードを想定します。
それぞれの点数を表すScoreと、点数を加算するaddPoint、結果を出力するgetResultを考えます。

createContextでContextを作成する

ReactのcreateContextでまずはContextを定義します。

1import React, { createContext } from 'react'
2...
3
4// 扱うStateを保持するContextを定義
5const ScoreBoardContext = createContext<ScoreBoardContextType>(null)

作成したContextのプロバイダーを定義する

作成したContextのプロバイダーを定義します。
後述しますが、このプロバイダー配下のコンポーネントでは、useContextを使うことでプロバイダー内のStateやメソッドにアクセスすることができます。

1import React, { createContext, useState } from 'react'
2...
3
4// プロバイダーを定義
5export const ScoreBoardProvider = ({ children }: { children: React.ReactNode }) => {
6  // 各チームのスコアを保持するStateを作成
7	const [score, setScore] = useState<Score>({
8    teamA: 0,
9    teamB: 0
10  })
11  // ポイントを加算する
12  const addPoint = (team: 'teamA'|'teamB', point: number) => {
13    setScore({
14      ...score,
15      [team]: score[team] + point
16    })
17  }
18  // 結果を表示する
19  const getResult = (): string => {
20    if (score.teamA === score.teamB) {
21      return `Draw`
22    } else {
23      return `${score.teamA > score.teamB ? 'teamA' : 'teamB'} Win!`
24    }
25  }
26	const value = {
27    score,
28    addPoint,
29    getResult
30	}
31  // valueにScoreBoardContextTypeの値を渡す
32	return <ScoreBoardContext.provider value={value}>{children}</ScoreBoardContext.provider>
33}

前述のScoreBoardContextTypeで定義した値とメソッド(score, addPoint, getResult)を、
ScoreBoardContext.providerのvalueに渡します。

useContextでフックを定義する

プロバイダーが定義できたら、 その配下でuseContext(ScoreBoaredContext)を呼ぶことで、ScoreBoardProviderのvalueにアクセスすることができます。 毎回useContext(ScoreBoaredContext)と書くのは面倒ですので、useScoreBoardフックを定義することで、簡潔にします。

1import React, { createContext, useContext, useState } from 'react'
2...
3// フックとして定義
4const useScoreBoard = () => useContext(ScoreBoardContext)

Context Hooksの使い方

  1. 作成したプロバイダーで、その値やメソッドを使いたいコンポーネントを囲む
  2. コンポーネントでContext Hooks(フック)を呼び出す

プロバイダーでコンポーネントを囲む

プロバイダーで囲まれたコンポーネント内ではContext Hooks(フック)が使用可能になります。

1import React from 'react'
2import { ScoreBoardProvider } from './useScoreBoard'
3import { TableTennisGame } from './TableTennisGame'
4
5// プロバイダーで囲む
6const App = () => {
7  <ScoreBoardProvider>
8    <TableTennisGame />
9  </ScoreBoardProvider>
10}

この例では卓球ゲーム(TableTennisGame)でスコアボードを使いたいので、ScoreBoardProviderで囲みました。

Context Hooks(フック)を呼び出す

プロバイダーで囲まれた配下のコンポーネントでは、フックを呼び出すことができます。

1import React from 'react'
2import { useScoreBoard } from 'useScoreBoard'
3
4// 卓球ゲーム
5export const TableTennisGame = () => {
6  // プロバイダー配下でContext Hooksを使う
7  const board = useScoreBoard()
8
9  // コンポーネント内で値やメソッドにアクセス可能になる
10  const pointA = () => board.addPoint('teamA', 1)
11  const pointB = () => board.addPoint('teamB', 1)
12  const showResult = () => {
13    window.alert(board.getResult())
14  }
15  return (
16    <>
17      <div>teamA: {board.score.teamA} / teamB: {board.score.teamA}</div>
18      <input type="button" onClick={pointA} value="pointA" />
19      <input type="button" onClick={pointB} value="pointB" />
20  )
21}

TableTennisGameでuseScoreBoardを呼び出すと、定義しておいたscoreや、addPointなどのメソッドを使用できるようになります。

Context Hooksまとめ

今回の例で作成したuseScoreBoard.tsxは以下の定義のセットになっています。

  • ScoreBoardContextType
  • ScoreBoardContext
  • ScoreBoardProvider
  • useScoreBoard

プロバイダーに囲まれた配下のコンポーネントでは、
useScoreBoardを呼び出すことで各値やメソッドを使うことができます。

Context Hooksのサンプル

1import React, { createContext, useContext, useState } from 'react'
2
3interface Score {
4  teamA: number,
5  teamB: number
6}
7// チームAとチームBのスコア記録したい
8interface ScoreBoardContextType {
9  score: Score
10  addPoint: (team: 'teamA'|'teamB', point: number) => void
11  getResult: () => string
12}
13
14// 扱うStateを保持するContextを定義
15const ScoreBoardContext = createContext<ScoreBoardContextType>(null)
16
17// プロバイダーを定義
18export const ScoreBoardProvider = ({ children }: { children: React.ReactNode }) => {
19  // 各チームのスコアを保持するStateを作成
20	const [score, setScore] = useState<Score>({
21    teamA: 0,
22    teamB: 0
23  })
24  // ポイントを加算する
25  const addPoint = (team: 'teamA'|'teamB', point: number) => {
26    setScore({
27      ...score,
28      [team]: score[team] + point
29    })
30  }
31  // 結果を表示する
32  const getResult = (): string => {
33    if (score.teamA === score.teamB) {
34      return `Draw`
35    } else {
36      return `${score.teamA > score.teamB ? 'teamA' : 'teamB'} Win!`
37    }
38  }
39	const value = {
40    score,
41    addPoint,
42    getResult
43	}
44  // valueにScoreBoardContextTypeの値を渡す
45	return <ScoreBoardContext.provider value={value}>{children}</ScoreBoardContext.provider>
46}
47
48// フックとして定義
49const useScoreBoard = () => useContext(ScoreBoardContext)