【React】useContext✖️useReducerでグローバルな状態管理を実現する。

# まえがき
Reactフックの`useContext`と`useReducer`を組み合わせて使えば、[Redux](https://react-redux.js.org/)や[Recoil](https://recoiljs.org/)等のライブラリを使わずにグローバル状態管理を実現することができる。本稿ではそれを実装するために必要な知識とコード例をまとめる。

# 参考記事

下記の記事がとても分かりやすくまとまっていました👏

https://qiita.com/ushi_osushi/items/98d1bc4a83265a764084

https://qiita.com/seira/items/fccdf4e73c59c491558d

https://qiita.com/seira/items/2fbad56e84bda885c84c

# 前提
今回扱う`useContext`と`useReducer`は**Reactフック**と呼ばれるものである。[他にもいろいろある。](https://ja.reactjs.org/docs/hooks-reference.html)

**useContext**
1箇所に定義されているデータを、複数箇所(コンポーネント)からグローバルに参照できるようにするReactフック(関数)。**参照できるようにするだけ**であって、更新はできない。

**useReducer**
複雑な状態管理を実現するもの。単純な状態管理は`useState`で事足りる。

## Reactフックとは
状態管理やライフサイクルのReact機能を、**クラスを書かずに**使えるようになる機能(**関数**)のこと。
　・Reactフックが登場する前までは、状態管理/ライフサイクルの機能を扱えるのは**クラスコンポーネントだけ**であった。
　・Reactフックが登場してからは、**関数コンポーネントでも**状態管理/ライフサイクルの機能を扱えるようになった。

## 関数コンポーネント vs クラスコンポーネント
下記のQlita記事、React公式にも記載してある通り、**コード量が少なく読みやすい**関数コンポーネントを使う。

https://qiita.com/ushi_osushi/items/98d1bc4a83265a764084

https://ja.reactjs.org/docs/hooks-faq.html#should-i-use-hooks-classes-or-a-mix-of-both

# useContext
1箇所に定義されているデータを、複数箇所(コンポーネント)からグローバルに参照できるようにするReactフック(関数)。**参照できるようにするだけ**であって、**更新はできない。**
## 使い方
親コンポーネント(App.tsx)に定義したデータを、複数の子コンポーネントから参照できるようにする。
```tsx:App.tsx(親コンポーネント)
import React, { createContext } from "react";
// 子コンポーネントを3つ用意。
import { AccountListSection } from "./accounts/AccountListSection";
import { MailListSection } from "./mails/MailListSection";
import { MessageListSection } from "./messages/MessageListSection";

type DataStoreContext = {
  accounts: string[];
  mails: string[];
  messages: string[];
};
// 参照できるようにするContextの初期値
const initialContext: DataStoreContext = {
  accounts: [],
  mails: [],
  messages: [],
};
// Context作成＆子コンポーネントから参照できるようにexportする。
export const DataStoreContext = createContext(initialContext);

const App = (): JSX.Element => {
  // 参照できるようにするContextのオブジェクト
  const contextState = {
    accounts: ["アカウントA", "アカウントB", "アカウントC"],
    mails: ["メールA", "メールB", "メールC"],
    messages: ["メッセージA", "メッセージB", "メッセージC"],
  };

  return (
    <div>
      <DataStoreContext.Provider value={contextState}>
        <AccountListSection />
        <MailListSection />
        <MessageListSection />
      </DataStoreContext.Provider>
    </div>
  );
}
```

```tsx:accounts/AccountListSection.tsx(子コンポーネント)
import { useContext } from "react";
import { DataStoreContext } from "../App";

export const AccountListSection = (): JSX.Element => {
  // 親(App.tsx)がexportしているDataStoreContextをINPUTに、useContextを実行。
  const dataStoreContext = useContext(DataStoreContext);
  return (
    <div>
      <h1>AccountListSection</h1>
      <p>{dataStoreContext.accounts.join(", ")}</p>
    </div>
  );
};
```
```tsx:mails/MailListSection.tsx(子コンポーネント)
import { useContext } from "react";
import { DataStoreContext } from "../App";

export const MailListSection = (): JSX.Element => {
  // 親(App.tsx)がexportしているDataStoreContextをINPUTに、useContextを実行。
  const dataStoreContext = useContext(DataStoreContext);
  return (
    <div>
      <h1>MailListSection</h1>
      <p>{dataStoreContext.mails.join(", ")}</p>
    </div>
  );
};
```
```tsx:messages:MessageListSection(子コンポーネント)
import { useContext } from "react";
import { DataStoreContext } from "../App";

export const MessageListSection = (): JSX.Element => {
  // 親(App.tsx)がexportしているDataStoreContextをINPUTに、useContextを実行。
  const dataStoreContext = useContext(DataStoreContext);
  return (
    <div>
      <h1>MessageListSection</h1>
      <p>{dataStoreContext.messages.join(", ")}</p>
    </div>
  );
};
```
起動するとこんな画面が表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653955/00b10183-cd7a-1e8d-aff2-7f89b47c77fb.png)

# useReducer
・状態管理をするためのReactフック。状態管理をする点で`useState`と同じ役割。
・`useState`と`useReducer`の違うところは**どうやってstateを更新するか。**
## useStateでの状態管理
・参照：直接stateオブジェクト(contactState)を参照する。
・更新：state更新用関数(setContactState)に、**値を直接を格納する。**
```ts
import { useState } from "react";

const initialContactState = {
  messages: [],
  mails: [],
}

const [contactState, setContactState] = useState(initialContactState),

// 参照
console.log(`messages: ${contactState.messages}`;
console.log(`mails: ${contactState.mails}`;
// 更新
setContactState({...contactState, messages: ["NEW MESSAGES"]});
setContactState({...contactState, mails: ["NEW MAILS"]});
```
## useReducerでの状態管理
・参照：直接stateオブジェクト(contactState)を参照する。
・更新：state更新用関数(dispatch)をコール with 更新するために必要な情報(type/payload)
　　　　**更新処理自体が行われるのはreducer関数内**。type/payloadをINPUTに複雑なロジックを
　　　　組みたい場合はこのreducer関数内に書いていく。
```ts
import { useState } from "react";

type ContactState = {
  messages: string[];
  mails: string[]
}

// Reducer関数
// ・受け取った引数を元に、stateを更新する。
//  第1引数： 更新前のstate
//  第2引数: dispatch関数の引数経由で送られたオブジェクト
const reducerFunc = (state, action) => {
  switch(action.type) {
    case "FETCH_MESSAGES":
      return {
         ...state,
         messages: action.payload
      };
    case "FETCH_MAILS":
      return {
         ...state,
         mails: action.payload
      };
    default:
      return state;
  }
}
// stateの初期値
const initialContactState = {
  messages: [],
  mails: [],
}

// useReducer()で、参照用のcontactState と 更新用のdispatch関数　を生成。
const [contactState, dispatch] = useReducer(reducerFunc, initialContactState);

// 参照
console.log(`messages: ${contactState.messages}`;
console.log(`mails: ${contactState.mails}`;
// 更新
dispatch({type: "FETCH_MESSAGES", payload: ["NEW MESSAGES"]});
dispatch({type: "FETCH_MAILS", payload: ["NEW MAILS"]});
```

## useStateとuseReducerの使い所
両方とも状態管理をするためのReactフック。それぞれどのタイミングで使うべきか。
|Reactフック|使い所|
|---|---|
|useState|・シンプルな状態を管理するとき(直接値を更新するようなもの)|
|useReducer|・状態管理するために複雑なロジックが必要になるもの<br>**・Redux,Recoil等を使わずにグローバルな状態管理を実現したい場合**|

Reactでは、グローバルな状態管理を実現するライブラリとして[Redux](https://react-redux.js.org/)や[Recoil](https://recoiljs.org/)が存在する。
しかし、これらのライブラリを利用せずとも、今まで説明にあげた`useReducer`と`useContext`の2つのReactフックを組み合わせればグローバルな状態管理を実現することができる。

# useContext✖️useReducerを組み合わせて使う。
実現したいこと
① APIで取得したデータを1箇所で管理する。
② 複数のコンポーネントから、①を参照＆更新できるようにする。

useContextとuseReducerをどう使う？
・useReducerで「参照用のstateオブジェクト」と「更新用のdispatch関数」を生成する。
・useContextで「参照用のstateオブジェクト」と「更新用のdispatch関数」を各コンポーネントからグローバルに参照できるようにする。

**useContextでは状態を参照することしかできなかったが、更新用のdispatch関数を参照させることで、各コンポーネントから「状態の参照＆状態の更新」を実現できるようにする。**

```tsx:context/DataStoreContext.tsx
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */

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

// useReducerで生成する「参照用のstate」の型
type DataStore = {
  isLoading: boolean;
  isError: boolean;
  accounts: string[];
  mails: string[];
  messages: string[];
};
// dispatch関数の第2引数に渡す「action」の型
type ReducerAction = {
  type: string;
  payload: any;
};
// createContext()のデフォルト値オブジェクトにasで割り当てる。
type DataStoreContext = {
  state: DataStore;
  // dispatchの引数オブジェクトの型を、React.Dispatch<XXXXX> に定義する。
  dispatch: React.Dispatch<ReducerAction>;
};

// reducer関数：更新用dispatchトリガーで、stateを更新する処理。
// 引数:   1.state 2.action(dispatch関数の引数)
// 戻り値: 更新後の新しいstate
const reducerFunc = (state: DataStore, action: ReducerAction) => {
  // action.typeの値で更新内容を切り替える。
  switch (action.type) {
    case "FETCH_ACCOUNTS":
      return {
        ...state,
        isLoading: true,
        accounts: action.payload,
      };
    case "FETCH_MAILS":
      return {
        ...state,
        isLoading: true,
        mails: action.payload,
      };
    case "FETCH_MESSAGES":
      return {
        ...state,
        isLoading: true,
        messages: action.payload,
      };
    // 更新前のstateをそのまま返す。
    default:
      return state;
  }
};
const initialState: DataStore = {
  isLoading: false,
  isError: false,
  accounts: ["INITIAL ACCOUNT"],
  mails: ["INITIAL_MAIL"],
  messages: ["INITIAL_MESSAGE"],
};

// createContextはReactフックではないため、コンポーネント外で使用可能
// as でオブジェクトの型チェックをクリアする。
export const DataStoreContext = createContext({} as DataStoreContext);

export const DataStoreContextProvider = (props): JSX.Element => {
  // useReducerで生成した「参照用state」と「更新用dispatch」を、contextに渡す。
  const [state, dispatch] = useReducer(reducerFunc, initialState);
  return (
    <DataStoreContext.Provider
      value={{
        state,
        dispatch,
      }}
    >
      {props.children}
    </DataStoreContext.Provider>
  );
};
```

```tsx:App.tsx(親コンポーネント)
import { DataStoreContextProvider } from "./context/DataStoreContext";
import { AccountListSection } from "./accounts/AccountListSection";
import { MailListSection } from "./mails/MailListSection";
import { MessageListSection } from "./messages/MessageListSection";

const App = (): JSX.Element => {
  return (
    <div>
      <DataStoreContextProvider>
        <AccountListSection />
        <MailListSection />
        <MessageListSection />
      </DataStoreContextProvider>
    </div>
  );
};

export default App;
```

```tsx:accounts/AccountListSection.tsx(子コンポーネント)
import { useContext } from "react";
import { DataStoreContext } from "../context/DataStoreContext";

export const AccountListSection = (): JSX.Element => {
  const { state, dispatch } = useContext(DataStoreContext);
  const getAccounts = () => {
    // useEffect関数内でAPIコール -> dispatchのpayloadに渡す。
    // (動作検証用なので今回はbuttonクリックをトリガーにする)
    dispatch({ type: "FETCH_ACCOUNTS", payload: ["NEW ACCOUNTS"] });
  };
  return (
    <div>
      <h1>AccountListSection</h1>
      <p>{state.accounts.join(", ")}</p>
      <button onClick={getAccounts}>GET NEW ACCOUNTS</button>
    </div>
  );
};
```

```tsx:mails/MailListSection.tsx(子コンポーネント)
import { useContext } from "react";
import { DataStoreContext } from "../context/DataStoreContext";

export const MailListSection = (): JSX.Element => {
  const { state, dispatch } = useContext(DataStoreContext);

  const getMails = () => {
    // useEffect関数内でAPIコール -> dispatchのpayloadに渡す。
    // (動作検証用なので今回はbuttonクリックをトリガーにする)
    dispatch({ type: "FETCH_MAILS", payload: ["NEW MAILS"] });
  };

  return (
    <div>
      <h1>MailListSection</h1>
      <p>{state.mails.join(", ")}</p>
      <button onClick={getMails}>GET NEW MAIL</button>
    </div>
  );
};
```

```tsx:messages/MessageListSection.tsx(子コンポーネント)
import { useContext } from "react";
import { DataStoreContext } from "../context/DataStoreContext";

export const MessageListSection = (): JSX.Element => {
  const { state, dispatch } = useContext(DataStoreContext);
  const getMessages = () => {
    // useEffect関数内でAPIコール -> dispatchのpayloadに渡す。
    // (動作検証用なので今回はbuttonクリックをトリガーにする)
    dispatch({ type: "FETCH_MESSAGES", payload: ["NEW MESSAGES"] });
  };
  return (
    <div>
      <h1>MessageListSection</h1>
      <p>{state.messages.join(", ")}</p>
      <button onClick={getMessages}>GET NEW MESSAGES</button>
    </div>
  );
};
```
アプリ起動
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653955/0ca0aafe-48fa-ed50-f642-3f83feb58168.png)
それぞれのボタンをクリックすると、dispatch関数で状態が更新される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653955/9d3de8b6-ad3d-acfe-d842-58801f97a9d6.png)


# 余談

## スプレッド構文(...state)
```ts
const initialState = {
  name: "UNKNOWN",
  age: 0,
  birthDate: "2000-01-01",
};

console.log({
  ...initialState,
  name: "山田太郎",
  age: 18,
});

// 出力結果 (nameとageは上書きされ、birthDataはそのまま)
{ name: '山田太郎', age: 18, birthDate: '2000-01-01' }
```