トップページへ

S.A.G.A佐賀!!出身者のプログラミングブログ

カウンターアプリを通してuseContextを理解する

カウンターアプリを通して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に置き換えると理解できるかと思います。

この記事の完成コードはこちら