useContextの基礎を理解する【React-Hooks】

React Hooks学習のアウトプット記事です。


## useContextとは

- stateをコンポーネント間で共有したい時に使用する
- propsのバケツリレーをしなくても、値を子コンポーネントに渡すことができる


## **基本的な使い方**

1.　データを保持したいContextを作成
```/src/Example.jsx
import { createContext } from "react";
import { Child } from "./components/Child";

// createContextを定義
export const ExampleContext = createContext("hello");

const Example = () => {
  return <Child />;
};

export default Example;
```


2.　子コンポーネントで呼び出す時
- `useContext` と作成したContext（`ExampleContext`）をインポート
- ExampleContextで設定した値を取得

```/components/Child.jsx
import { useContext } from "react";
import { ExampleContext } from "../Example";

export const Child = () => {
  // ExampleContextで設定した値を取得する
  const value = useContext(ExampleContext);

  return (
    <p>{value}</p> // "hello"
  );
};
```

## 実際に使ってみる

ラジオボタンの切替で色が変わる仕組みを`useContext` を使って作ってみる。

![useContext.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3772772/d45922f4-7532-d041-d811-5438bfbc66d4.gif)


### 1.　子コンポーネントで使うための準備

- `ThemeContext`の作成
- `ThemeProvider` を作成し、この中で持ち回りたい状態変数を宣言
（これを親コンポーネントで呼び出し、子コンポーネントをラップする）
- `ThemeContext`の呼び出し関数を作成
（子コンポーネントごとに呼び出すこともできるが、ここでまとめておいた方が◎）

```src/context/ThemeContext.tsx
import React, { createContext, useContext, useState } from "react";

// 型定義
type ThemeProviderProps = {
  children: React.ReactNode;
};

// ThemeContextを作成
export const ThemeContext = createContext<
  [string, React.Dispatch<React.SetStateAction<string>>] | undefined
>(undefined);

// 親コンポーネントで呼び出し、子コンポーネントにラップする
export const ThemeProvider = ({ children }: ThemeProviderProps) => {
  // テーマの状態管理
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={[theme, setTheme]}>
      {children}
    </ThemeContext.Provider>
  );
};

// ThemeContextの呼び出し（子コンポーネントで呼び出すときに使用）
export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error("ThemeProviderの外で呼び出されました");
  }
  return context;
};

```

### 2. 親コンポーネントでの記述

- ThemeContext.tsx で作成した、`ThemeProvider` 関数を呼び出し、子コンポーネントをラップする

```src/App.js
import { Header } from "./components/Header";
import { Main } from "./components/Main";
import { ThemeProvider } from "./context/ThemeContext";
import "./styles.css";

export default function App() {
  return (
    <main className="App">
      <ThemeProvider>
        {/* ここにラップされたコンポーネントは、コンテキストが使える */}
        <Header />
      </ThemeProvider>
    </main>
  );
}

```

### 3. 子コンポーネントでの記述

- ThemeContext.tsx で作成した、`useTheme` 関数を呼び出す
- `useTheme` 関数から、使いたい変数を取り出す

```src/components/Header.tsx
import React from "react";
import "../styles.css";
import { useTheme } from "../context/ThemeContext";

export const Header = () => {
  // 選択できるテーマ
  const THEMES: string[] = ["light", "dark", "red"];

  // ThemeContext.tsx で作成した、useTheme 関数を呼び出す
  const [theme, setTheme] = useTheme();
  // const [,setTheme] = useTheme(); // setTheme だけ取り出したい場合はこのように書ける

  // 選択変更で theme の状態を更新
  const changeTheme = (e) => setTheme(e.target.value);

  return (
    <header className={`content-${theme}`}>
      {THEMES.map((_theme) => (
        <label key={_theme}>
          <input
            type="radio"
            value={_theme}
            checked={theme === _theme}
            onChange={changeTheme}
          />
          {_theme}
        </label>
      ))}
    </header>
  );
};
```


## 注意点

- ステートが更新されると、子コンポーネントは全て再レンダリングされる
  => 不必要な再レンダリングにより、パフォーマンスが下がる恐れ

### 対処法

コンテキストとプロバイダーを、「状態の値（state）」と「更新用関数（setState）」とで分割することで、
テーマとテーマの更新関数を分けて使用できるようになる

```src/context/ThemeContext.tsx
import React, {
  createContext,
  Dispatch,
  SetStateAction,
  useContext,
  useState,
} from "react";

// 型定義
type ThemeProviderProps = {
  children: React.ReactNode;
};

// ThemeContextを作成
// stateの状態値
export const ThemeContext = createContext<string | undefined>(undefined);
// 更新用関数
export const UpdateThemeContext = createContext<
  Dispatch<SetStateAction<string>> | undefined
>(undefined);

// 親コンポーネントで呼び出し、子コンポーネントにラップする
export const ThemeProvider = ({ children }: ThemeProviderProps) => {
  // テーマの状態管理
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={theme}>
      <UpdateThemeContext.Provider value={setTheme}>
        {children}
      </UpdateThemeContext.Provider>
    </ThemeContext.Provider>
  );
};

// ThemeContextの呼び出し（子コンポーネントで呼び出すときに使用）
// stateの状態値の呼び出し
export const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error("ThemeProviderの外で呼び出されました");
  }
  return context;
};

// 更新用関数の呼び出し
export const useUpdateTheme = () => {
  const context = useContext(UpdateThemeContext);
  if (!context) {
    throw new Error("ThemeProviderの外で呼び出されました");
  }
  return context;
};
```

（そこまで規模の大きいアプリケーションを作ったことがないので実感はないが、
増えすぎるとこのファイルを読み解くのが難しくなりそうな…）

---

以前ユーザー情報を持ち回る処理を書いている時に使いました。
また使う前に、改めて理解したかったので、分からなくなくなったらこの記事を読み返します。