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> </> ) }