useReducer を書きながら学ぶ（React Hooks 入門シリーズ 6/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)
- [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) :point_left_tone2: 今回

これが最後です。

## はじめに

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

## useReducer とは

- `useState`と同じ様な状態管理用のhook
    - 基本的に、`useState`でできることは`useReducer`にもできます
- `state`と`dispatch`（actionを送信する関数）を返す

### 構文

```js
const [state, dispatch] = useReducer(reducer, initialState)
```

- reducer: state を 更新するための関数
- dispatch: reducer を 実行するための呼び出し関数
　- dispath には action（何を実行するかをまとめたオブジェクト） を引数を渡す

## 実践1...カウントアプリ（state 1つ）

`useState`でも実装できるカウントアプリを今回は`useReducer`を使って実装していきましょう。

### 手順

- `initialState`の定義
- `reducer`の定義
- `useReducer`の定義
- JSXの記述（actionを表示するボタン、カウント数(state)を表示する部分）

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

```js
import React, { useReducer } from "react";

const initialState = 0;

const reducer = (state, action) => {
  switch (action) {
    case "increment":
      return state + 1;
    case "decrement":
      return state - 1;
    case "reset":
      return initialState;
    default:
      return state;
  }
};

const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      <p>{state}</p>
      <button onClick={() => {dispatch('increment')}} >+</button>
      <button onClick={() => {dispatch('decrement')}} >-</button>
      <button onClick={() => {dispatch('reset')}} >reset</button>
    </div>
  );
};

export default App;
```

![275b5aeca20974dab556afc12e2e19a7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/3016e03c-f5e6-03c6-a3f4-c4ffee769d31.gif)

`initialState`が0で、各`reducer`がちゃんと機能してることがわかります。

</div></details>

## 実践2...カウントアプリ（複数のstate）

実践1を少し発展させた内容で、複数の`state`を持たせていきましょう。

`usereducer`が`useState`より好ましいのは、複数の値を管理する複雑なステートロジックがある場合や、前のステートに基づいて、次のステートを決める必要がある場合です。

これを頭に入れておけば最適な使い分けの一助になるでしょう。

### 手順

- ① `initialState`の定義
- ② JSXにinitialStateの配置
- ③ dispatch で渡している actionの中にペイロード（アクションの実行に必要な任意のデータ）を含ませる
- ④ `reducer`の定義
    - 今回はactionにtypeとvalueをセットしているので、Switch分のcaseの宣言は`action`→`action.type`に変更
    - `secondCounter`用にcaseを増やす
    - 複数のstateを含むので、spread構文を用いて`prevState`を展開し、更新します
- ⑤ `useReducer`の定義（前と同じ）
- JSXの記述（actionを表示するボタン、カウント数(state)を表示する部分）

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

```js
import React, { useReducer } from "react";

// ① `initialState`の定義
const initialState = {
  firstCounter: 0,
  secondCounter: 10
};

const reducer = (state, action) => {
  // ④ actionにtypeとvalueをセットしているので、Switch分のcaseの宣言は`action`→`action.type`に変更
  switch (action.type) {
    case "increment1":
      return { ...state, firstCounter: state.firstCounter + action.value }; // 複数のstateを含むので、spread構文を用いて`prevState`を展開し、更新します
    case "decrement1":
      return { ...state, firstCounter: state.firstCounter - action.value }; // 上に同じ
    case "increment2":
      return { ...state, secondCounter: state.secondCounter + action.value }; // 上に同じ
    case "decrement2":
      return { ...state, secondCounter: state.secondCounter - action.value }; // 上に同じ
    case "reset":
      return initialState;
    default:
      return state;
  }
};

const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      {/* ② JSXにinitialStateの配置 */}
      <p>カウント1: {state.firstCounter}</p>
      <p>カウント2: {state.secondCounter}</p>
　　　 {/* ③ dispatch で渡している actionの中にペイロード（アクションの実行に必要な任意のデータ）を含ませる */}
      {/* ③ action 内を Object にして、typeとvalueをセットする */}
      <button onClick={() => {dispatch({ type: "increment1", value: 1 })}}>
        +increment1
      </button>
      <button onClick={() => {dispatch({ type: "decrement1", value: 1 })}}>
        -decrement1
      </button>
      <button onClick={() => {dispatch({ type: "increment2", value: 10 })}}>
        +increment2
      </button>
      <button onClick={() => {dispatch({ type: "decrement2", value: 10 })}}>
        -decrement2
      </button>
      <button onClick={() => {dispatch({ type: "reset" })}}>
        reset
      </button>
    </div>
  );
};

export default App;
```

![eca7242260f0b62c9a0b9ed96826c0e5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/3356fbe6-263f-febc-2337-48b7ad9c242a.gif)

</div></details>

## 実践3...useReducer × useContext

useReducer で（ローカルに）扱ってきたStateを useContext を使って （グローバルに）Stateを扱っていきたいと思います。

![IMG_7DA18755378F-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/b966f35f-f890-d824-b61f-00c638102187.jpeg)

今回も前回の実践2と同様カウントアプリを作ります。（グローバルに）Stateにカウント値を持たせて各、子コンポネントで値の参照・更新ができるようにします。

![29f613e15e8518fdecc5d72a170823e6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/591dcf4a-14d6-122b-cb98-45c9e003ed0f.gif)

### 手順

- 3つの子コンポネント A〜Cの作成
- 親コンポネントから子コンポネント3つをimport
- `reducer`と`initialState`の定義
- `useReducer`に`reducer`と`initialState`を渡すことでcountStateとdispatch関数を作成
- createContextを使ってcountContextを作成（exportする）
- countContextを使って Provider を用意して valueには countとdispatchをセットする
- 各子コンポネントでは
    - 渡されたcountContextをuseContextしてもちいる
    - valueからcountを表示させとdispatch関数からincrement, decrementを使えるようにする


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

```js:App.js
import React, { useReducer, createContext, createElement } from "react";
import { ComponentA } from "./components/ComponentA";
import { ComponentB } from "./components/ComponentB";
import { ComponentC } from "./components/ComponentC";

export const CountContext = createContext();

const initialState = {
  firstCounter: 0
};

const reducer = (state, action) => {
  switch (action.type) {
    case "increment1":
      return { ...state, firstCounter: state.firstCounter + action.value };
    case "decrement1":
      return { ...state, firstCounter: state.firstCounter - action.value };
    case "reset":
      return initialState;
    default:
      return state;
  }
};

const App = () => {
  const [count, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      <h1>カウント：{count.firstCounter}</h1>
      <CountContext.Provider
        value={{ countState: count, countDispatch: dispatch }}
      >
        <ComponentA />
        <ComponentB />
        <ComponentC />
      </CountContext.Provider>
    </div>
  );
};

export default App;
```

```js:components/ComponentA
import React, { useContext } from "react";
import { CountContext } from "../App";

export const ComponentA = () => {
  const countContext = useContext(CountContext);
  return (
    <div>
      <p>ComponentA</p>
      <button
        onClick={() => {
          countContext.countDispatch({ type: "increment1", value: 1 });
        }}
      >
        +increment1
      </button>
      <button
        onClick={() => {
          countContext.countDispatch({ type: "decrement1", value: 1 });
        }}
      >
        -decrement1
      </button>
      <button
        onClick={() => {
          countContext.countDispatch({ type: "reset" });
        }}
      >
        reset
      </button>
    </div>
  );
};
```

```js:components/ComponentB
import React, { useContext } from "react";
import { CountContext } from "../App";

export const ComponentB = () => {
  const countContext = useContext(CountContext);
  return (
    <div>
      <p>ComponentB</p>
      <button
        onClick={() => {
          countContext.countDispatch({ type: "increment1", value: 1 });
        }}
      >
        +increment1
      </button>
      <button
        onClick={() => {
          countContext.countDispatch({ type: "decrement1", value: 1 });
        }}
      >
        -decrement1
      </button>
      <button
        onClick={() => {
          countContext.countDispatch({ type: "reset" });
        }}
      >
        reset
      </button>
    </div>
  );
};
```

```js:components/ComponentC
import React, { useContext } from "react";
import { CountContext } from "../App";

export const ComponentC = () => {
  const countContext = useContext(CountContext);
  return (
    <div>
      <p>ComponentC</p>
      <button
        onClick={() => {
          countContext.countDispatch({ type: "increment1", value: 1 });
        }}
      >
        +increment1
      </button>
      <button
        onClick={() => {
          countContext.countDispatch({ type: "decrement1", value: 1 });
        }}
      >
        -decrement1
      </button>
      <button
        onClick={() => {
          countContext.countDispatch({ type: "reset" });
        }}
      >
        reset
      </button>
    </div>
  );
};
```

### 実行結果

![29f613e15e8518fdecc5d72a170823e6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/591dcf4a-14d6-122b-cb98-45c9e003ed0f.gif)

それぞれの子コンポネントの動作が確認できました。こうすることで、`useReducer`と`useContext`を使ってグローバルにstateを管理することができます。

</div></details>

## 実践4...useReducer × 非同期処理（useEffect）

`useReducer`と`useEffect`を使って非同期処理させて外部APIからデータ（記事の擬似データ）を取得・表示させてみましょう。

![b2e512d2cb1ee20debac0bb334563ba6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/b6f57997-0ef5-6ed9-a334-42de63371b6b.gif)

### 手順

- `useReducer`と`useEffect`のimport
- 非同期処理は`axios`を使う
- `initialState`の定義（loading, error, post）
- reducerの作成
    - 引数（`state`,`action`）
    - return 新しいstate
    - switch ケースは HTTPリクエストの成功時（`FETCH_SUCCESS`） or 失敗時（`FETCH_ERROR`）で処理を分けます
- `useReducer`に`initialState`と`reducer`関数を読み込ませて、`state`と`dispatch`関数を利用できるようにします
- `useEffect`内にHTTPリクエスト非同期処理を記載する
    - 記事の擬似データを取得できる[JSONPlaceholder](https://jsonplaceholder.typicode.com/)からデータをとってくる（`GET`リクエストで）
    - 今回は[このURLにGETリクエスト](https://jsonplaceholder.typicode.com/posts/1)します
- JSXに、`FETCH_SUCCESS`時と`FETCH_ERROR`時の場合のコンポーネントを書く

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

```js:App.js
import React, { useEffect, useReducer } from "react";
import axios from "axios";

const initialState = {
  loading: true,
  error: "",
  post: {}
};

const reducer = (state, action) => {
  switch (action.type) {
    case "FETCH_SUCCESS":
      return {
        loading: false,
        error: "",
        post: action.payload
      };
    case "FETCH_ERROR":
      return {
        loading: false,
        error: "データ取得に失敗",
        post: {}
      };
    default:
      return state;
  }
};

const App = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    axios
      .get("https://jsonplaceholder.typicode.com/posts/1")
      .then((res) => {
        dispatch({ type: "FETCH_SUCCESS", payload: res.data });
      })
      .catch(() => {
        dispatch({ type: "FETCH_ERROR" });
      });
  }, []);

  return (
    <div>
      <h1>{state.loading ? "Loading..." : state.post.title}</h1>
      <p>{state.error ? state.error : null}</p>
    </div>
  );
};

export default App;
```

### 実行結果

![b2e512d2cb1ee20debac0bb334563ba6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14191/b6f57997-0ef5-6ed9-a334-42de63371b6b.gif)

非同期処理なので、時間差で 「Loading」が`title`に変わっていることがわかります。

</div></details>

## シーン別 useState と useReducer 使い分け

結論、シンプルなstateには`useState`が向いていて、複雑なstate管理には`useReducer`がいいでしょう。

表にして、特徴をまとめました。

| | useState | useReducer |
|:-----------|:------------|:------------|
| 扱うといい state の型       | Number, String, Bool などのプリミティブ型を更新する時 | Array, Object の更新に使う時 |
| どんなシーンで使うといい？     | 単独のstate（複数でもできるっちゃできる）      | 複数のstateを同時に更新したい時       |
| stateの更新に複雑なビジネスロジックがある場合 | 好ましくない | 好ましい  |
| ストアーの種類         | ローカルな管理に向いてる          | useContextと合わせてグローバルに管理するとよい |

以上です！これで最後です。

## 連載の最後に

シリーズ全部読んでくれた方へ。

どうもありがとうございました。

## 参考

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