useStateだけの場合

よくあるカウンターコンポーネントを作ってみます。
useStateでカウントを更新できるようにしています。

import React, { useState } from 'react'

const Parent: React.FC = () => {
	const [count, setCount] = useState(0)
	return (
		<div>
			<h1>カウント: { count }</h1>
			<button onClick={() => setCount(count + 1)}>+</button>
			<button onClick={() => setCount(count - 1)}>-</button>
		</div>
	)
}

export default Parent

コンポーネントを分けて孫のコンポーネントにボタンを設置してみましょう。

import React, { useState } from 'react'

const Parent: React.FC = () => {
	const [count, setCount] = useState(0)
	return (
		<div>
			<h1>カウント: { count }</h1>
			<Child count={count} setCount={setCount} />
		</div>
	)
}

type Props = {
	count: number
	setCount:  React.Dispatch<React.SetStateAction<number>>
}

const Child: React.FC<Props>  = ({count, setCount}) => {
	return (
		<>
			<p>子コンポーネント</p>
			<Grandchild count={count} setCount={setCount} />
		</>
	)
}

const Grandchild: React.FC<Props>  = ({count, setCount}) => {
	return (
		<>
			<p>孫コンポーネント</p>
			<button onClick={() => setCount(count + 1)}>+</button>
			<button onClick={() => setCount(count - 1)}>-</button>
		</>
	)
}

useContextを使用した場合

子コンポーネントではcountを使用しないのですが、中継するためにPropsを受け取って渡さなければいけないので無駄に感じますね。
useContextを使うことでstateを孫コンポーネントから直接アクセスできるようにしてみましょう。

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

const CountContext = createContext({} as {
	count: number
	setCount: React.Dispatch<React.SetStateAction<number>>
})
	
const Parent: React.FC = () => {
	const [count, setCount] = useState(0)
	return (
		<CountContext.Provider value={{ count, setCount }}>
			<h1>カウント: { count }</h1>
			<Child />
		</CountContext.Provider>
	)
}
	
const Child: React.FC  = () => {
	return (
		<>
			<p>子コンポーネント</p>
			<Grandchild />
		</>
	)
}
	
const Grandchild: React.FC = () => {
	const { count, setCount } = useContext(CountContext)
	
	return (
		<>
			<p>孫コンポーネント</p>
			<button onClick={() => setCount(count + 1)}>+</button>
			<button onClick={() => setCount(count - 1)}>-</button>
		</>
	)
}
8行目
createContextでContextを使用できるようにします。
TypeScriptの場合どのような型を使用するのかも宣言しましょう。
13行目
CountContext.Providerで囲むことでその配下のコンポーネントでContextにアクセスできるようになります。
30行目
useContextを使用することで、Contextにアクセスできるようになります。

useReducerを使用した場合

これでStateを使用しない子コンポーネントではPropsの受け渡しの記述が必要なくなりました。
今回はcountとsetCountだけなのでこれでも問題ないのですが、今後使用するステートが増えた場合はContextを増やすか、Valueを増やすかということになりちょっといけてない感が出てきそうです。
そこで登場するのがuseReducerです。

最初にuseReducerだけを使用した例をみてみましょう。

import React, { useReducer } from 'react'

type State = {
	count: number
};
	
type Action =
{ type: 'INCREMENT' } |
{ type: 'DECREMENT' }

const reducer = (state: State, action: Action) => {
	switch(action.type) {
		case 'INCREMENT':
			return {
				...state,
				count: state.count + 1
			}
		case 'DECREMENT':
			return {
				...state,
				count: state.count - 1
			}
		default:
			return state
	}
}

const initialState = {
	count: 0
}

const Parent: React.FC = () => {
	const [state, dispatch] = useReducer(reducer, initialState)

	return (
		<div>
			<h1>カウント: { state.count }</h1>
			<button onClick={() => dispatch({type:'INCREMENT'})}>+</button>
			<button onClick={() => dispatch({type:'DECREMENT'})}>-</button>
		</div>
	)
}

export default Parent

ポイントはreducerとしてステートの更新をまとめていることと、ステートも一つにまとめていることです。(このサンプルでは一つのステートしかないのであれですが。)
またこのreducerを実行するにはdispatchを使用します。

useContext/useReducerを組み合わせてみる

最後にuseReducerをuseContextと組み合わせて孫コンポーネントからもdispatchを実行できるようにしてみましょう。

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

type State = {
	count: number
}
	
type Action =
{ type: 'INCREMENT' } |
{ type: 'DECREMENT' }

const reducer = (state: State, action: Action) => {
	switch(action.type) {
		case 'INCREMENT':
			return {
				...state,
				count: state.count + 1
			}
		case 'DECREMENT':
			return {
				...state,
				count: state.count - 1
			}
		default:
			return state
	}
}

const AppContext = createContext({} as {
	state: State
	dispatch: React.Dispatch<Action>
})

const initialState = {
	count: 0
}

const Parent: React.FC = () => {
	const [state, dispatch] = useReducer(reducer, initialState)
	return (
		<AppContext.Provider value={{ state, dispatch }}>
			<h1>カウント: { state.count }</h1>
			<Child />
		</AppContext.Provider>
	)
}

const Child: React.FC  = () => {
	return (
		<>
			<p>子コンポーネント</p>
			<Grandchild />
		</>
	)
}

const Grandchild: React.FC = () => {
	const { dispatch } = useContext(AppContext)
	return (
		<>
			<p>孫コンポーネント</p>
			<button onClick={() => dispatch({type:'INCREMENT'})}>+</button>
			<button onClick={() => dispatch({type:'DECREMENT'})}>-</button>
		</>
	)
}