Reactを再考する#4

## これまで

[第一回](https://qiita.com/a2-tsuchiya/items/7a827b0b5487c9db2b75): ```useState```, ```useRef```
[第二回](https://qiita.com/a2-tsuchiya/items/fe5260ae2ea8de67cb77): ```useContext```
[第三回](https://qiita.com/a2-tsuchiya/items/506f0f6c7c77917547c3): ```useEffect```, ```useLayoutEffect```, ```useMemo```, ```useCallback```

今回は、```useReducer```です。
ステート管理のキモとなるフックですね。。

## 前提

- React 16.8以降。なので**Hook API**の使用が前提です
- Reactの基本的な説明はしません
- TypeScriptで書いてます

## 申し訳程度の説明

- [公式](https://ja.reactjs.org/docs/hooks-reference.html#usereducer)には```useState```の代替品とあります。実際、```useState```は```useReducer```で実装されています。
- ```useReducer```
    - 構文
        -  ```[state, dispatch] = useReducer(reducer, initialState)```
    - 引数
        - ```reducer```: 現在のステート値を受け取って、新しいステート値を返す関数
        - ```initialState```: ステートの初期値
    - 戻り値
        - ```state```: ステート値
        - ```dispatch```: `reducer`を実行するための関数

## 使ってみる

簡単なチェックボックスを作り、```useState```と比較してみます。

**useState**

```tsx
import React, { useState } from "react";

export default function Checkbox() {
  const [checked, setChecked] = useState(false);
  const toggle = () => setChecked(checked => !checked)

  return (
    <>
      <input type="checkbox" checked={checked} onChange={toggle} />
      {checked ? "checked" : "not checked"}
    </>
  );
}
```

**useReducer**

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

export default function Checkbox() {
  const [checked, toggle] = useReducer((checked) => !checked, false);

  return (
    <>
      <input type="checkbox" checked={checked} onChange={toggle} />
      {checked ? "checked" : "not checked"}
    </>
  );
}
```

なんかステート更新のロジックが抽象化されただけですね。```useState```で十分な気が。。

### ちょっと複雑なステートを考えてみる

簡単なユーザ情報を表示させるコンポーネントを考えます。

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

/** 管理したいステート */
type State = {
  id: string;
  firstName: string;
  lastName: string;
  city: string;
  state: string;
  email: string;
  admin: boolean;
};
type Actions = Partial<State>;

interface Reducer {
  (user: State, newDetails: Actions): State;
}

/** ステートの初期値 */
const firstUser: State = {
  id: "0391-3233-3201",
  firstName: "Taro",
  lastName: "Yamada",
  city: "Tokyo",
  state: "Shinjuku",
  email: "t-yamada@react.com",
  admin: false
};

/** ステートを受け取りステートを返す関数 */
const reducer: Reducer = (user, newDetails) => ({ ...user, ...newDetails });

function User() {
  const [user, setUser] = useReducer(reducer, firstUser);
  const handler = () => setUser({ admin: true });

　　　　// もしもuseStateで記述していたら、、、
  // const handler = () => setUser({ ...user, admin: true });

  return (
    <div>
      <h1>
        {user.firstName} {user.lastName} - {user.admin ? "Admin" : "User"}
      </h1>
      <p>Email: {user.email}</p>
      <p>
        Location: {user.city}, {user.state}
      </p>
      <button onClick={handler}>あなたも今日から管理人！</button>
    </div>
  );
}

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

ステート更新にいちいちスプレッド構文を使わなくていいのでスッキリしますね。
が、まだ```useState```で事足りそうですよね。。

### もっと複雑なステートを考えてみる

前述の例に加え、複数のオブジェクトをステートを管理する場合を考えます。
`useContext`と```useReducer```を組み合わせて`store`的にステートを一元管理します。

**./src/AppProvider.tsx**

```tsx
import React, { createContext, useContext, useReducer } from "react";

type IUser = {
  id: string;
  firstName: string;
  lastName: string;
  city: string;
  state: string;
  email: string;
  admin: boolean;
};

/** 管理したいステート */
type State = {
  user: IUser;
  login: false;
  // other
};
type Actions = Partial<State>;

interface AppContextType {
  state: State;
  dispatch: React.Dispatch<Actions>;
}

/** ステートの更新関数 */
const reducer = (state: State, actions: Actions) => {
  const newState = { ...state };
  if(actions.user) newState.user = actions.user
  if(actions.login) newState.login = actions.login

  return newState;
};

/** ステートの初期値 */
const initState = {} as State;

/** コンテキストをエクスポート */
const AppContext = createContext({} as AppContextType);
export const useAppContext = () => useContext(AppContext);

/** コンテキスト */
const AppContextProvider: React.FC = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initState);
  return (
    <AppContext.Provider value={{ state, dispatch }}>
      {children}
    </AppContext.Provider>
  );
};
export default AppContextProvider;

```

**./src/index.tsx**

```tsx
import React from "react";
import { render } from "react-dom";
import AppProvider from "./AppProvider"
import App from "./App";

render(
  <AppProvider>
    <App />
  </AppProvider>,
  document.getElementById("root")
);
```

**./src/App.tsx**

```tsx
import React, { useEffect } from "react";
import { useAppContext } from "./AppProvider";

function User() {
  const { state, dispatch } = useAppContext();
  const { user } = state;
  const handler = () =>
    dispatch({
      user: { ...user, admin: true }
    });

  useEffect(() => dispatch({ login: true }));

  return (
    <div>
      <h1>
        {user.firstName} {user.lastName} - {user.admin ? "Admin" : "User"}
      </h1>
      <p>Email: {user.email}</p>
      <p>
        Location: {user.city}, {user.state}
      </p>
      <button onClick={handler}>あなたも今日から管理人！</button>
    </div>
  );
}

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

ステートのスケールを可能にしつつ、一元管理する方法としてはイイ線じゃないでしょうか。。


## 参考文献

[Reactハンズオンラーニング 第2版 ――Webアプリケーション開発のベストプラクティス](https://www.oreilly.co.jp/books/9784873119380/)