useContextをuseState・useReducer・関数と組み合わせる方法

JavaScript

前回、【React】useContexでContextを参照する手順でReact Contextの概要について紹介しました。
実際のアプリケーションでは、useContextuseStateuseReducer・関数などと組み合わせて利用することが多いです。
そこで今回は具体例を交えてuseContextとの組み合わせ方法について紹介します。

なお、ContextはExampleProviderコンポーネントで作成し、以下のようにアプリケーション全体で囲われている前提とします。

src/pages/_app.tsx

import "../../styles/globals.css";
import type { AppProps } from "next/app";
import { ExampleProvider } from "src/components/shared/ExampleProvider";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ExampleProvider>
      <Component {...pageProps} />
    </ExampleProvider>
  );
}

export default MyApp;

useStateと組み合わせるケース

Contextの作成方法

クリックでカウントアップするカウンタアプリケーションは以下のように実装できます。

src/components/shared/ExampleProvider.tsx

import React, { createContext, FC, ReactNode, useState } from "react";

type Props = {
  children: ReactNode;
}

type ContextType = {
  setCount: (value: number) => void;
  count: number;
}

export const ExampleContext = createContext<ContextType>({} as ContextType);

export const ExampleProvider: FC<Props> = ({children}) => {
  const [count, setCount] = useState<number>(0);

  return (
    <ExampleContext.Provider value={{count, setCount}}>
      {children}
    </ExampleContext.Provider>
  )
};

上記のように、useStateで取得したStateおよびState更新メソッドをvalueにセットすることでContextとuseStateを組み合わせられます。

Contextの利用方法

setCount(stateを更新する関数)をContextから受け取り、setCountの処理をコンポーネントに実装することでStateを更新できます。

src/pages/index.tsx

import type { NextPage } from "next";
import { ExampleContext } from "src/components/shared/ExampleProvider";
import { useContext } from "react";
import styles from "../../styles/Home.module.css";

const Home: NextPage = () => {
  const { count, setCount } = useContext(ExampleContext);
  return (
    <div className={styles.main}>
      <div>{count}</div>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
};

export default Home;

なおsetCount(count + 1)setCount((prevCount) => prevCount + 1)と実装する場合、setCountの型はsetCount: (value: React.SetStateAction<number>) => voidとします。

setCount((prevCount) => prevCount + 1)に関する説明は【React】そろそろ技術ブログで setCount(count + 1) と書くのはやめませんかを参考にしてください。

関数と組み合わせるケース

Providerで定義した関数をContext経由で他のコンポーネントに渡すこともできます。

Contextの作成方法

以下はincrementというsetCount(count + 1)を実行する関数とContextを組み合わせた例です。

src/components/shared/ExampleProvider.tsx

import React, { createContext, FC, ReactNode, useState } from "react";

type Props = {
  children: ReactNode;
};

type ContextType = {
  increment: () => void;
  count: number;
};

export const ExampleContext = createContext<ContextType>({} as ContextType);

export const ExampleProvider: FC<Props> = ({ children }) => {
  const [count, setCount] = useState<number>(0);

  const increment = () => {
    setCount(count + 1);
  };

  return (
    <ExampleContext.Provider value={{ count, increment }}>
      {children}
    </ExampleContext.Provider>
  );
};

Contextの利用方法

Contextから受けとった関数を実行することでStateが更新されます。

src/pages/index.tsx

import type { NextPage } from "next";
import { ExampleContext } from "src/components/shared/ExampleProvider";
import { useContext } from "react";
import styles from "../../styles/Home.module.css";

const Home: NextPage = () => {
  const { count, increment } = useContext(ExampleContext);
  return (
    <div className={styles.main}>
      <div>{count}</div>
      <button onClick={increment}>+</button>
    </div>
  );
};

export default Home;

先ほど紹介した「useStateと組み合わせるケース」ではsetCountの実装をContextを利用するコンポーネントで実装する必要がありました。
しかし関数とContextを組み合わせることにより、Contextを利用するコンポーネントはStateの更新ロジックを気にする必要がなくなります。

useReducerと組み合わせるケース

useReducerは、より複雑なStateを管理したい場合に利用するuseStateの代替となるHooksです。
useReducerはReducer関数((state, action) => newStateという型をもつ関数)を受け取り、StateとDispatch関数のペアを返します。

Contextと組み合わせることで、StateとDispatch関数がグローバルで扱えます。
その結果、Reduxのように「各コンポーネントでActionを実行し、グローバルなStateを取得する」というアプリケーションが実現できます。

Contextの作成方法

カウンタの値をStateで管理し、値を変更する処理をDispath関数で定義した例について考えます。

import React, { createContext, FC, ReactNode, useReducer } from "react";

//========
// Action
//========
const ActionTypes = {
  increment: "increment",
  decrement: "decrement",
} as const;

type ActionTypes = typeof ActionTypes[keyof typeof ActionTypes];

type IncrementActionType = {
  type: "increment";
};

type DecrementActionType = {
  type: "decrement";
};

type CounterActionTypes = IncrementActionType | DecrementActionType;

//========
// State
//========
type StateType = {
  count: number;
};

const initialState: StateType = {
  count: 0,
};

//========
// Reducer
//========
const reducer = (state: StateType, action: CounterActionTypes): StateType => {
  switch (action.type) {
    case ActionTypes.increment: {
      return { count: state.count + 1 };
    }
    case ActionTypes.decrement: {
      return { count: state.count - 1 };
    }
    default:
      throw new Error();
  }
};

//========
// Context
//========
type ContextType = {
  state: StateType;
  dispatch: React.Dispatch<CounterActionTypes>;
};

export const ExampleContext = createContext<ContextType>({} as ContextType);

//========
// Provider
//========
type Props = {
  children: ReactNode;
};

export const ExampleProvider: FC<Props> = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <ExampleContext.Provider value={{ state, dispatch }}>
      {children}
    </ExampleContext.Provider>
  );
};

上記のように、useReducerで取得したStateとDispatch関数をvalueにセットすることでContextとuseReducerを組み合わせられます。
Contextの型(ここでいうContextType)のプロパティにはStateとDispatch関数を定義します。

Contextの利用方法

受け取ったDispatch関数を利用してActionを実行することでStateが更新されます。

import type { NextPage } from "next";
import { ExampleContext } from "src/components/shared/ExampleProvider";
import { useContext } from "react";
import styles from "../../styles/Home.module.css";

const Home: NextPage = () => {
  const { state, dispatch } = useContext(ExampleContext);
  return (
    <div className={styles.main}>
      <div>{state.count}</div>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
    </div>
  );
};

export default Home;

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!

参考資料