ReactのContext Hooksを使って状態管理を実現する(createContext, useContext)
Reactの状態管理のためのパッケージとしてはReduxやRecoilなどがありますが、そのようなパッケージを使わなくてもReactのContext(Hooks、フック)を作って状態管理を実現することができます。
Context Hooks作成の流れ
- 扱いたい値やメソッドをInterfaceで定義する
- createContextでContextを作成する
- 作成したContextのプロバイダーを定義する
- 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の使い方
- 作成したプロバイダーで、その値やメソッドを使いたいコンポーネントを囲む
- コンポーネントで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)