カウンターアプリを通してuseContextを理解する
Reactでは定義した値を子コンポーネントで使用するには、親コンポーネントからpropsで渡す必要があります。 しかし階層が深くなると、そのコンポーネントにpropsを渡すまでバケツリレーをすることになってしまいます。 そこでグローバルに状態を保持することでpropsのバケツリレーをせずアプリケーションを開発することができます。 Reactで使用できるグローバルの状態管理にはReduxやZustandなど様々なライブラリがありますが、今回はReact hooksであるuseContextを使用します。 useContextはReact既定のグローバルな状態管理ができるhooksです。 簡単なカウンターアプリケーションをuseContextで管理するように書き換えて使い方を理解します。
現在のカウンターアプリのコード
useContextに置き換える前に現在カウンターアプリケーションがどのようになっているか見てみましょう。
src/App.ts
import { useReducer } from 'react';
import { Counter } from './components/counter/Counter';
function App() {
type Action = {
type: '+' | '-';
step: number;
};
const reducer = (prev: number, { type, step }: Action) => {
switch (type) {
case '+':
return prev + step;
case '-':
return prev - step;
default:
throw new Error('Error');
}
};
const initialState = 0;
const [state, dispatch] = useReducer(reducer, initialState);
const countUp = () => {
dispatch({ type: '+', step: 1 });
};
const countDown = () => {
dispatch({ type: '-', step: 1 });
};
return (
<>
<Counter state={state} countUp={countUp} countDown={countDown} />
</>
);
}
export default App;
src/components/counter/Counter.tsx
import type { FC } from 'react';
import { CounterButton } from './CounterButton';
import { CounterCount } from './CounterCount';
type Props = {
state: number;
countDown: () => void;
countUp: () => void;
};
export const Counter: FC<Props> = ({ state, countDown, countUp }) => {
return (
<>
<CounterCount state={state} />
<CounterButton step={1} type="+" onClick={countUp} />
<CounterButton step={1} type="-" onClick={countDown} />
</>
);
};
src/components/counter/CounterButton.tsx
import type { FC } from 'react';
type Props = {
onClick: () => void;
type: '+' | '-';
step: number;
};
export const CounterButton: FC<Props> = ({ onClick, type, step }) => {
return (
<button onClick={onClick}>
{type}
{step}
</button>
);
};
src/components/counter/CounterCount.tsx
import type { FC } from 'react';
type Props = {
state: number;
};
export const CounterCount: FC<Props> = ({ state }) => {
return <div>{state}</div>;
};
現在App.tsxで定義しpropsで渡しているので、propsを経由せずuseContextを使用して値を渡せるように変更していきます。
contextを作成する
useContextを使うにはcreateContextでcontextを作成する必要があります。 今回はstateとdispatchで2つのcontextを作成します。 contextを作成するファイルを用意しそこに処理を記述していきましょう。
src/context/CounterContext.tsx
import { createContext } from "react";
const CounterContext = createContext();
const CounterDispatchContext = createContext()
Providerに渡す
上記で作成したcontextをuseContextで使用するにはProviderのvalueに渡す必要があります。 また今回はTypeScriptを使用しているのでcontextを作成するとcreateContextにデフォルト値を入れるように警告が出ています。 デフォルト値はProviderが見つからなかったときに使用される値になっています。 デフォルト値を入れるとともに型の整合性をとるためApp.tsxとCounterProvider.tsxを修正し、App.tsxの一部をCounterReducer.tsに移すようにします。
src/App.tsx
import { Counter } from './components/counter/Counter';
function App() {
const countUp = () => {
dispatch({ type: '+', step: 1 });
};
const countDown = () => {
dispatch({ type: '-', step: 1 });
};
return (
<>
<Counter state={state} countUp={countUp} countDown={countDown} />
</>
);
}
export default App;
src/components/CounterReducer.ts
import { useReducer } from 'react';
type Action = {
type: '+' | '-';
step: number;
};
const reducer = (prev: number, { type, step }: Action) => {
switch (type) {
case '+':
return prev + step;
case '-':
return prev - step;
default:
throw new Error('Error');
}
};
const initialState = 0;
export const useCounterReducer = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return { state, dispatch };
};
export const defaultCounterReducer: ReturnType<typeof useCounterReducer> = {
state: initialState,
dispatch: () => {},
};
src/context/CounterContext.tsx
import { createContext, FC } from 'react';
import { defaultCounterReducer, useCounterReducer } from '../components/counter/CounterReducer';
const { state, dispatch } = defaultCounterReducer;
const CounterContext = createContext(state);
const CounterDispatchContext = createContext(dispatch);
type Props = {
children: React.ReactNode;
};
export const CounterProvider: FC<Props> = ({ children }) => {
const { state, dispatch } = useCounterReducer();
return (
<CounterContext.Provider value={state}>
<CounterDispatchContext.Provider value={dispatch}>
{children}
</CounterDispatchContext.Provider>
</CounterContext.Provider>
);
};
useContextで利用する
上記でCounterContext.tsxはエラーが解消されました。App.tsxにはエラーが残っていますがApp.tsxでProviderを読み込むときにこちらの修正をしようと思いますので一旦進めます。 Providerを作成しvalueにstateとdispatchを渡したのでこれをuseContextで利用できるようにします。
src/context/CounterContext.tsx
import { createContext, FC, useContext } from 'react';
import { defaultCounterReducer, useCounterReducer } from '../components/counter/CounterReducer';
const { state, dispatch } = defaultCounterReducer;
const CounterContext = createContext(state);
const CounterDispatchContext = createContext(dispatch);
type Props = {
children: React.ReactNode;
};
export const CounterProvider: FC<Props> = ({ children }) => {
const { state, dispatch } = useCounterReducer();
return (
<CounterContext.Provider value={state}>
<CounterDispatchContext.Provider value={dispatch}>
{children}
</CounterDispatchContext.Provider>
</CounterContext.Provider>
);
};
export const useCounter = () => useContext(CounterContext);
export const useCounterDispatch = () => useContext(CounterDispatchContext);
App.tsxでProviderを読み込む
CounterコンポーネントでProviderのvalueに設定した値を使用したいのでApp.tsxでProviderを読み込みます。
src/App.tsx
import { Counter } from './components/counter/Counter';
import { CounterProvider } from './context/CounterContext';
function App() {
const countUp = () => {
dispatch({ type: '+', step: 1 });
};
const countDown = () => {
dispatch({ type: '-', step: 1 });
};
return (
<CounterProvider>
<Counter state={state} countUp={countUp} countDown={countDown} />
</CounterProvider>
);
}
export default App;
propsからuseContextに変更する
ここまででCounterコンポーネントでstateとdispatchを使用することができるようになりました。 App.tsxからpropsで渡しているところをuseContext経由で使用するように変更し、countUpとcountDown関数をCounterButton.tsxに移します。
src/App.tsx
import { Counter } from './components/counter/Counter';
import { CounterProvider } from './context/CounterContext';
function App() {
return (
<CounterProvider>
<Counter />
</CounterProvider>
);
}
export default App;
src/components/counter/CounterButton.tsx
import type { FC } from 'react';
import { useCounterDispatch } from '../../context/CounterContext';
type Props = {
type: '+' | '-';
step: number;
};
export const CounterButton: FC<Props> = ({ type, step }) => {
const dispatch = useCounterDispatch();
const clickHandler = () => dispatch({ type, step });
return (
<button onClick={clickHandler}>
{type}
{step}
</button>
);
};
countUpとcountDownはclickHandler関数に置き換えました。 他のコンポーネントもpropsからuseContextに置き換えましょう。
src/components/counter/Counter.tsx
import type { FC } from 'react';
import { CounterButton } from './CounterButton';
import { CounterCount } from './CounterCount';
export const Counter: FC = () => {
return (
<>
<CounterCount />
<CounterButton step={1} type="+" />
<CounterButton step={1} type="-" />
</>
);
};
src/components/counter/CounterCount.tsx
import type { FC } from 'react';
import { useCounter } from '../../context/CounterContext';
export const CounterCount: FC = () => {
const state = useCounter();
return <div>{state}</div>;
};
今回のカウンターの場合だと複雑ではないためuseContextを使わずともpropsでも大丈夫そうですが、propsのバケツリレーがなくなりスッキリとしました。 コードばかりで説明が少ないですが実際にpropsからcontextに置き換えると理解できるかと思います。
この記事の完成コードはこちら