Context / useContext を書きながら学ぶ を書きながら学ぶ（React Hooks 入門シリーズ 2/6）

## ロードマップ

React 16.8 で追加された機能であるReactの`Hooks`について書いてあります。

書きながら学ぶ React Hooks 入門シリーズとして書き下ろしました。

- [React.memo / useCallback / useMemo](https://qiita.com/ryosuketter/items/c503d98e16c28f516c4d)
- [Context / useContext](https://qiita.com/ryosuketter/items/90bd9edd5d91b8daf5c9) :point_left_tone2: 今回
- [useRef](https://qiita.com/ryosuketter/items/f2cc554cc89fb41d5a0b)
- [useState](https://qiita.com/ryosuketter/items/40dfc139ed90ebffd284)
- [useEffect](https://qiita.com/ryosuketter/items/6551ee0d75a3da003980)
- [useReducer](https://qiita.com/ryosuketter/items/3d59f1c503fe27fa0cb9)

## はじめに

Reactの組み込みフックである`Context`と`useContext`の説明をします。

また、`useContext`は、Reactの`Context`と併用するので、`Context`の解説もします。

## Contextとは

`useContext`は、`Context`というReactの仕組みを利用するために必要なフックです。

なので、まずは`Context`について説明します。

`Context`とは...と言っても様々な言い方ができるでしょう。

- 「状態」と「状態を変更するメソッド」を、propsを用いず、アプリケーション全体で取り回すことができるやつ
- Propsを利用せずに、様々な階層のコンポーネントに値を共有するReactのAPI

などなどですね。図で説明しましょう。

本をReactで表現するとします。

![IMG_56E1E941470B-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/f83be8cd-9cf5-a38c-2202-bea8482a23d8.jpeg)

ホントは、もっといろいろなコンポネントがありえますが、今回は、Bookのデータ（仮に`bookData`とする）を`<Page />`と`<Title />`で、使いたいとします。

![IMG_1BF3BF95601E-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/8ab9e431-eba8-7d24-febb-fc2b94857f3c.jpeg)

すると、このようにpropsをバケツリレーして渡す方法もあります。ですが、`<Body />`と`<Cover />`はそんなデータを使用しない。とか、もっと多階層で中間のコンポネントは`bookData`をまったく使わないので、バケツリレーで渡す意味がない時ありますよね？

もしくは、グローバルにどこでも取りまわせるstateを持ちたい時がありますよね？

そんな時は、`Context`の出番です。

![IMG_9AACCBB1127C-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/b4631a7f-7e79-1987-c131-6643aa32c0fd.jpeg)

`Context`がデータストアの役割をはたし、propsを使わず直接`bookData`を下層階層のコンポネントが使うことができるようにしたものです。

`useContext`は、`Context`の機能をさらにシンプルに使うことができるやつです。

### Contextを利用するために必要なもの

- Contextオブジェクト： `React.createContext`というReactのAPIの戻り値
- Provider： Contextオブジェクトが保持しているコンポネント
- Consumer： Contextオブジェクトから値を取得しているコンポネント

### サンプルコード

基本の状態は以下にします。（Bookを表すには雑すぎますが...ツリー構造さえわかればOKなので許してください）

![スクリーンショット 2021-01-28 23.10.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/529628dd-c201-8142-4e10-80326aac3510.png)

Contextを利用して、下層コンポネントに、データを受け渡します。
以下の順番で書いていきます。

- createContext（`BookContext`）してexportしておく（Book コンポネントで実装）
- Providerコンポネントを作成し、valueにオブジェクト（state）をセットします（Book コンポネントで実装）
- 下層コンポネントで、先ほど作成したBookContextをimportします（Title コンポネントで実装）
- importしたBookContextを使ってconsumerを作成（Title コンポネントで実装）
    - Consumerコンポネント内でbook state の中身にアクセスできます

赤枠が、変更点です👇

![Context___useContext_を書きながら学ぶ_-_CodeSandbox.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/ec62dfb0-d951-686b-95b6-d43669c4a3b4.png)

実行結果はこんな感じです。

![スクリーンショット 2021-01-28 23.40.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/71fe2503-2f0d-5834-011e-88d55c36e751.png)

propsで上の階層から渡していないのに、下階層でcreateContextしたデータが引っ張れていることがわかります。

それでは次に、`useContext`を使っていきましょう。

## useContextとは

`useContext`を使った場合でも、Providerを使って値を渡す点は同じです。

構文はこんな感じです。

```js
const Contextオブジェクトの値 = useContext(Contextオブジェクト)
```

Contextオブジェクトから、取得できる値は、Contextオブジェクトが保持しているProviderのvalueプロパティに指定された値です。

#### サンプルコード

`<Cover />`とその子供の`<Title />`は、createContextされた`BookContext`と`AuthorContext`コンテキストを`Privide`されているので、それぞれのデータを引っ張ることができます。このように、複数のコンテキストを扱うことも可能です。

<details><summary>サンプルコード</summary><div>

```js:/components/Book.js
import React, { createContext, useState } from "react";
import Cover from "./Cover";
import Body from "./Body";

const bookData = {
  author: "ryosuketter",
  title: "how to use React context",
  isbn: 12345,
  yearOfPublication: 2021
};

const authorData = {
  name: "ryosuketter",
  age: 35,
  gender: "male"
};

export const BookContext = createContext();
export const AuthorContext = createContext();

const Book = () => {
  const [book, setBook] = useState(bookData);
  const [author, setAuthor] = useState(authorData);
  return (
    <>
      <BookContext.Provider value={book}>
        <AuthorContext.Provider value={author}>
          <Cover />
        </AuthorContext.Provider>
        <Body />
      </BookContext.Provider>
    </>
  );
};

export default Book;
```

```js:components/Cover/Title.js
import React, { useContext } from "react";
import { BookContext, AuthorContext } from "../../Book";

const Title = () => {
  const book = useContext(BookContext);
  const author = useContext(AuthorContext);

  return (
    <div>
      <p>this is 「{book.title}」</p>
      <p>author is {author.name}</p>
      <p>age is {author.age}</p>
    </div>
  );
};

export default Title;
```

</div></details>

特定のコンポネントで、Contextの追加や上書きをしたデータを作成して使うことも可能です。

<details><summary>サンプルコード</summary><div>

```js:/components/Book.js
// 上と同じ
```

```js:components/Body/Page.js
import Reac, { useContext } from "react";
import { BookContext } from "../../Book";

const Page = () => {
  const book = useContext(BookContext);
  const customedBookContext = {
    ...book,
    author: "ryosuke",
    publisher: "Qiita publications"
  };

  return (
    <div>
      <p>this is page</p>
      <p>author is {customedBookContext.author}</p>
      <p>publisher is {customedBookContext.publisher}</p>
    </div>
  );
};

export default Page;
```

</div></details>

上2つのコードの実行結果

![スクリーンショット 2021-01-29 1.04.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/d0da28c3-0e10-ef1a-8295-7f0898b4d4d5.png)

### Context利用時の注意点（Context更新時に不要な再レンダーを招く）

Contextは使い方によっては、パフォーマンスの問題を引き起こす可能性があります。

なぜから、**Provider内のすべてのConsumerは、Providerのvalueプロパティが更新するたびに再レンダリングするから**です。

特に、以下の場合は注意です

- 再レンダリングされる Consumer の数が多い場合
- Consumerの子コンポネントのレンダリングコストが高い場合

不要な再レンダリングを防ぐ方法は、次の3つです。

- Contextオブジェクトの分割
- React.memoの利用
- useMemoの利用

ためにし、1つ実際にやってみましょう。

#### useMemoの利用をすることで不要な再レンダリングを防ぐ方法

仮に、本の著者の情報に対して、いいねボタン（+1カウントアップ）があるとします。

![e7c7326683a1101eacaecdf0e275293c.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/9ade4d6b-5acc-43ee-fd92-821cd1e72406.gif)

変更を行うのは`/components/Book.js`と`/components/Cover/Title.js`です（他のコードは、これまでと変わりません）。
Provider内のすべてのConsumerは、Providerのvalueプロパティが更新するたびに再レンダリングされるので、
下記のままでは、+1カウントアップ されるたびに、`<Title />`コンポネントも再レンダリングされます。

<details><summary>サンプルコード</summary><div>

```js:/components/Book.js
import React, { createContext, useState } from "react";
import Cover from "./Cover";
import Body from "./Body";

const bookData = {
  author: "ryosuketter",
  title: "how to use React context",
  isbn: 12345,
  yearOfPublication: 2021
};

const authorData = {
  name: "ryosuketter",
  age: 35,
  gender: "male",
  like: 0
};

export const BookContext = createContext();
export const AuthorContext = createContext();

const Book = () => {
  const [book] = useState(bookData);
  const [author, setAuthor] = useState(authorData);
  return (
    <>
      <BookContext.Provider value={book}>
        <AuthorContext.Provider value={author}>
          <Cover />
        </AuthorContext.Provider>
        <Body />
      </BookContext.Provider>
      <p>Like: {author.like}</p>
      <button onClick={() => setAuthor({ ...author, like: author.like + 1 })}>
        +
      </button>
    </>
  );
};

export default Book;

```

```js:components/Body/Title.js
import React, { useContext } from "react";
import { BookContext, AuthorContext } from "../../Book";

const Title = () => {
  const book = useContext(BookContext);
  const author = useContext(AuthorContext);

  console.log("render");
  return (
    <div>
      <p>this is 「{book.title}」</p>
      <p>author is {author.name}</p>
      <p>age is {author.age}</p>
    </div>
  );
};

export default Title;
```
</div></details>

useMemoの利用をすることで不要な再レンダリングを防ぐ方法は、以下

<details><summary>サンプルコード</summary><div>

```js:components/Body/Title.js
import React, { useContext, useMemo } from "react";
import { BookContext, AuthorContext } from "../../Book";

const Title = () => {
  const book = useContext(BookContext);
  const author = useContext(AuthorContext);

  return useMemo(() => {
    console.log("render");
    return (
      <div>
        <p>this is 「{book.title}」</p>
        <p>author is {author.name}</p>
        <p>age is {author.age}</p>
      </div>
    );
  }, [book.title, author.name, author.age]);
};

export default Title;
```
</div></details>

useMemoの`[deps]`に、`[book.title, author.name, author.age]`を入れることで、こいつらが更新されない限り、メモ化されたデータが返されて、再レンダリングを防いでいます。

![fd9af052eceedf1713eee933dad1b9ef.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/f50a9274-36ac-3e3b-1de1-db1a0b092e60.gif)

ぜひ、試しながらやってみてください。

今回は以上です。

## 参考

https://ja.reactjs.org/tutorial/tutorial.html