React Context APIの使い方

## 1. この記事について


React公式の状態管理機能であるContext APIの使い方メモです
ReactとRemixでの利用について書きます


## 2. Context APIとは


prop drillingダルいよね
という時にReact標準で使えるstate管理機能です
Reduxと似たようなことが出来るようです


## 3. ReactでContext APIを使う


以下の3つのステップでstateを共有できます


1. 渡したいstateや更新する為の関数を定義したContextを作る
2. Contextを渡す範囲をProviderで指定する
3. useContextでContextを呼び出してstateや関数を使う


### 3-1. ContextとProviderをつくる


React.createContextでContextを作り、これをProviderに組み込んで使います


```react:context.tsx
import React, { useState, ReactNode } from 'react';

type CountContextType = {
  count: number;
  setCount: (newCount: number) => void;
} | null
export const CountContext = React.createContext<CountContextType>(null);

export function CountProvider({ children }: { children: ReactNode }) {
  const [count, setCount] = useState<number>(0);

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

### 3-2. Contextを渡す範囲をProviderで指定する


Providerで囲んでやれば囲まれているすべてのコンポーネントでContextが利用可能になります


```react:app.tsx
import { CountProvider } from './context'
export function App() {
  return (
    <CountProvider>
      <ComponentA />
      <ComponentB />
    </CountProvider>
  )
}
```


上記の場合はComponentAとComponentBで利用可能になります
直接でなく子コンポーネントや孫コンポーネントでも利用可能です


### 3-3. useContextで呼び出して利用する


useContextで作成済みのContextを呼んでくるとProviderを定義したときにvalueに渡した値や関数を受け取れます。useState単独でなく、複数のstateやもっと複雑な処理をする関数を作って共有することも出来ます


```react
import { useContext } from 'react';
import { CountContext } from './context'

export default function ComponentA() {
  const context = useContext(CountContext);

  if (context === null) {
    throw new Error('CountButton must be used within a CountProvider');
  }

  const { count, setCount } = context;

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

  return (
    <div>
      <button onClick={incrementCount}>Click me!</button>
      <p>ComponentA {count}</p>
    </div>
  );
}

export default function ComponentB() {
  const context = useContext(CountContext);

  if (context === null) {
    throw new Error('CountButton must be used within a CountProvider');
  }

  const { count, setCount } = context;

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

  return (
    <div>
      <button onClick={incrementCount}>Click me!</button>
      <p>ComponentB {count}</p>
    </div>
  );
}
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/46a28043-1510-8755-4985-3fc8bd5f4b1c.png)


同じstateを共有しているのでどちらをクリックしても両方がカウントアップされるようになりました


## 4. RemixでContext APIを使う


RemixのNested Routeのようにナビゲーションやサイドバーとメイン画面を分けて書くときもContext APIが使えます


### 4-1. ContextとProviderをつくる


ここは同じです


### 4-2. Contextを渡す範囲をProviderで指定する


Providerで囲んでやれば良いのは同じです


```react:/root.tsx
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";
import "./tailwind.css";

import React, { useContext } from 'react';
import { CountProvider, CountContext } from './routes/context'

function Header() {
  const context = useContext(CountContext);

  if (context === null) {
    throw new Error('CountButton must be used within a CountProvider');
  }

  const { count } = context;

  return (
    <header>
      <nav>
        へっだー部分です: { count }
      </nav>
    </header>
  )
}

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <CountProvider>
          <Header />
          {children}
        </CountProvider>
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export default function App() {
  return <Outlet />;
}
```


### 4-3. useContextで呼び出して利用する


後は使いたいコンポーネントで呼び出して使うだけです


```react:/routes/_index.tsx
import { useContext } from 'react';
import { CountContext } from './context'


export default function Index() {
  const context = useContext(CountContext);

  if (context === null) {
    throw new Error('CountButton must be used within a CountProvider');
  }

  const { count, setCount } = context;

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

  return (
    <div>
      <button onClick={incrementCount}>Click me!</button>
      <p>本文部分です {count}</p>
    </div>
  );
}
```


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277233/8f90277a-bbdf-3be3-bf7f-e5c1b153feee.png)


できました


## 5. まとめ


prop drillingに悩まされていたのが解消できそうです

また、Contextを分けて書いておけるのが分かりやすく、リファクタリングや設計変更でアプリのコンポーネント構造が変わるときにもすごく役立ちそう

コンポーネント内でのみ使うstateは普通にコンポーネントで書いておく方が簡単なので、使い分けるのが良さげです