← Blog一覧へ

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を定義しておきます。

// スコアの定義
interface Score {
  teamA: number,
  teamB: number
}
// チームAとチームBのスコア記録したい
interface ScoreBoardContextType {
  score: Score
  addPoint: (team: 'teamA'|'teamB', point: number) => void
  getResult: () => string
}

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

createContextでContextを作成する

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

import React, { createContext } from 'react'
...

// 扱うStateを保持するContextを定義
const ScoreBoardContext = createContext<ScoreBoardContextType>(null)

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

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

import React, { createContext, useState } from 'react'
...

// プロバイダーを定義
export const ScoreBoardProvider = ({ children }: { children: React.ReactNode }) => {
  // 各チームのスコアを保持するStateを作成
	const [score, setScore] = useState<Score>({
    teamA: 0,
    teamB: 0
  })
  // ポイントを加算する
  const addPoint = (team: 'teamA'|'teamB', point: number) => {
    setScore({
      ...score,
      [team]: score[team] + point
    })
  }
  // 結果を表示する
  const getResult = (): string => {
    if (score.teamA === score.teamB) {
      return `Draw`
    } else {
      return `${score.teamA > score.teamB ? 'teamA' : 'teamB'} Win!`
    }
  }
	const value = {
    score,
    addPoint,
    getResult
	}
  // valueにScoreBoardContextTypeの値を渡す
	return <ScoreBoardContext.provider value={value}>{children}</ScoreBoardContext.provider>
}

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

useContextでフックを定義する

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

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

Context Hooksの使い方

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

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

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

import React from 'react'
import { ScoreBoardProvider } from './useScoreBoard'
import { TableTennisGame } from './TableTennisGame'

// プロバイダーで囲む
const App = () => {
  <ScoreBoardProvider>
    <TableTennisGame />
  </ScoreBoardProvider>
}

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

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

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

import React from 'react'
import { useScoreBoard } from 'useScoreBoard'

// 卓球ゲーム
export const TableTennisGame = () => {
  // プロバイダー配下でContext Hooksを使う
  const board = useScoreBoard()

  // コンポーネント内で値やメソッドにアクセス可能になる
  const pointA = () => board.addPoint('teamA', 1)
  const pointB = () => board.addPoint('teamB', 1)
  const showResult = () => {
    window.alert(board.getResult())
  }
  return (
    <>
      <div>teamA: {board.score.teamA} / teamB: {board.score.teamA}</div>
      <input type="button" onClick={pointA} value="pointA" />
      <input type="button" onClick={pointB} value="pointB" />
  )
}

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

Context Hooksまとめ

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

  • ScoreBoardContextType
  • ScoreBoardContext
  • ScoreBoardProvider
  • useScoreBoard

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

Context Hooksのサンプル

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

interface Score {
  teamA: number,
  teamB: number
}
// チームAとチームBのスコア記録したい
interface ScoreBoardContextType {
  score: Score
  addPoint: (team: 'teamA'|'teamB', point: number) => void
  getResult: () => string
}

// 扱うStateを保持するContextを定義
const ScoreBoardContext = createContext<ScoreBoardContextType>(null)

// プロバイダーを定義
export const ScoreBoardProvider = ({ children }: { children: React.ReactNode }) => {
  // 各チームのスコアを保持するStateを作成
	const [score, setScore] = useState<Score>({
    teamA: 0,
    teamB: 0
  })
  // ポイントを加算する
  const addPoint = (team: 'teamA'|'teamB', point: number) => {
    setScore({
      ...score,
      [team]: score[team] + point
    })
  }
  // 結果を表示する
  const getResult = (): string => {
    if (score.teamA === score.teamB) {
      return `Draw`
    } else {
      return `${score.teamA > score.teamB ? 'teamA' : 'teamB'} Win!`
    }
  }
	const value = {
    score,
    addPoint,
    getResult
	}
  // valueにScoreBoardContextTypeの値を渡す
	return <ScoreBoardContext.provider value={value}>{children}</ScoreBoardContext.provider>
}

// フックとして定義
const useScoreBoard = () => useContext(ScoreBoardContext)