React の useReducer を活用して state が複雑化するのを避ける

この記事は [CodeChrysalis Advent Calendar 2019](https://qiita.com/advent-calendar/2019/codechrysalis) の記事です。

# はじめに

[Eliaが書いてくれたstateが配列やオブジェクトの場合の扱い方](https://qiita.com/maaaashin324/items/08c74d8664b39455dc38)にインスパイアされて、この記事を書くことにしました。

前提知識としてreduxの知識（store, dispatch, reducer）を理解している前提とします。

# Motivation

state を単純化するほうが良いですが、どうしても複雑化する場合もあります。例えばユーザーの情報を登録する場合のフォームを用意している場合、

* 名前
* 名前かな
* 電話番号
* 郵便番号
* 住所

等々の情報を必要とします。これらさえもそれぞれで `useState` を使えば単純化できますが、例えばすでにデータベースに登録しているユーザー情報をGETして、それに対して編集を加える場合、GETしたオブジェクトのそれぞれを、それぞれの属性ごとのstateに入れることもできますが、オブジェクトごと変更したい場合は `useReducer` が使えます。

# コード

Component

```tsx
import React, { useReducer } from 'react';
import { initFormState, formReducer, ReducerType } from './UserFormReducer';
import { UserFormInterface } from './UserFormInterface';

const Signup: React.FC<{}> = () => {
  const [formState, formDispatch] = useReducer<React.Reducer<UserFormInterface, ReducerType>>(
    formReducer,
    initFormState()
  );
  ...
```

stateを初期化する関数

```ts
export const initFormState = (user?: Partial<UserType>): UserFormInterface => ({
  familyName: !user || !user.familyName ? '' : user.familyName,
  givenName: !user || !user.givenName ? '' : user.givenName,
  familyKana: !user || !user.familyKana ? '' : user.familyKana,
  givenKana: !user || !user.givenKana ? '' : user.givenKana,
  address1: !user || !user.address1 || user.address1 === '未設定' ? '' : user.address1,
  address2: !user || !user.address2 || user.address2 === '未設定' ? '' : user.address2,
  email: !user || !user.userEmail ? '' : user.userEmail,
});
```

stateを初期化する関数のアウトプットの型を表す型

```ts
export interface UserFormInterface {
  familyName: string;
  givenName: string;
  familyKana: string;
  givenKana: string;
  address1: string;
  address2: string;
  email: string;
}
```

ReducerのType

```ts
export type ReducerType =
  | { type: 'familyName'; payload: string | undefined }
  | { type: 'givenName'; payload: string | undefined }
  | { type: 'familyKana'; payload: string | undefined }
  | { type: 'givenKana'; payload: string | undefined }
  | { type: 'address1'; payload: string | undefined }
  | { type: 'address2'; payload: string | undefined }
  | { type: 'email'; payload: string | undefined }
  | { type: 'reset' };
```

Reducer

```ts
export const formReducer = (state, action) => {
  switch (action.type) {
    case 'familyName':
      return { ...state, familyName: action.payload };
    case 'givenName':
      return { ...state, givenName: action.payload };
    case 'familyKana':
      return { ...state, familyKana: action.payload };
    case 'givenKana':
      return { ...state, givenKana: action.payload };
    case 'address1':
      return { ...state, address1: action.payload };
    case 'address2':
      return { ...state, address2: action.payload };
    case 'email':
      return { ...state, email: action.payload };
    case 'reset':
      return initFormState();
    default:
      return { ...state };
  }
};
```

## useReducerの使い方

useReducerのFunction signatureを紹介します。

### Input

第一引数：reducer
第二引数：stateを初期化した結果

### Output

配列です。`useState`と似ていますが、以下の内容となっています。

インデックス０：stateそのもの
インデックス１：dispatcher

そしてもしstateを書き換えたい場合は、dispatcherを使います。上記のコードの例だと`formDispatch`です。そしてrenderするときに`formState`を使ってたとえば`<input>`の`value`に割り当てることによって、変更した値をレンダリングすることができます。

## 要素の解説

必要なのは（TypeScriptで書いているということもあって）以下です。

* stateを初期化する関数
* stateを初期化する関数のアウトプットの型を表す型
* reducer
* reducerの型

### stateを初期化する関数

再掲です。

```ts
export const initFormState = (user?: Partial<UserType>): UserFormInterface => ({
  familyName: !user || !user.familyName ? '' : user.familyName,
  givenName: !user || !user.givenName ? '' : user.givenName,
  familyKana: !user || !user.familyKana ? '' : user.familyKana,
  givenKana: !user || !user.givenKana ? '' : user.givenKana,
  address1: !user || !user.address1 ? '' : user.address1,
  address2: !user || !user.address2 ? '' : user.address2,
  email: !user || !user.userEmail ? '' : user.userEmail,
});
```

ポイントは登録でも更新でも使えるように、引数は`user?: Partial<UserType>`としていて、引数があってもなくても良いようにしています。

ちなみにここでは[Ternary Operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator)を使っていますが、最新版のTypescriptであれば[nullish-coalescing](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#nullish-coalescing)を使うと、もう少しエレガントに書けます。最新版の[Create React App](https://github.com/facebook/create-react-app/releases/tag/v3.3.0)を使えば使えます。

Outputはユーザーの情報を表すオブジェクトとしています。もし引数にユーザーの情報が引き渡されていればそれを含み、もし含まれていなければ初期化された（何も含まない）オブジェクトとなります。

### stateを初期化する関数のアウトプットの型を表す型

再掲です。

```ts
export interface UserFormInterface {
  familyName: string;
  givenName: string;
  familyKana: string;
  givenKana: string;
  address1: string;
  address2: string;
  email: string;
}
```

これを定義してあげると、Componentでintellisenseが働いて、Developing experienceが爆上がりします。

### reducer

```ts
export const formReducer = (state, action) => {
  switch (action.type) {
    case 'familyName':
      return { ...state, familyName: action.payload };
    case 'givenName':
      return { ...state, givenName: action.payload };
    case 'familyKana':
      return { ...state, familyKana: action.payload };
    case 'givenKana':
      return { ...state, givenKana: action.payload };
    case 'address1':
      return { ...state, address1: action.payload };
    case 'address2':
      return { ...state, address2: action.payload };
    case 'email':
      return { ...state, email: action.payload };
    case 'reset':
      return initFormState();
    default:
      return { ...state };
  }
};
```

Reduxを理解していれば、そのものですね。`state`のオブジェクトをspread operatorを使って新しいオブジェクトに作り直し、さらにreducerに渡された更新内容でアップデートしています。

### reducerの型

再掲です。

```ts
export type ReducerType =
  | { type: 'familyName'; payload: string | undefined }
  | { type: 'givenName'; payload: string | undefined }
  | { type: 'familyKana'; payload: string | undefined }
  | { type: 'givenKana'; payload: string | undefined }
  | { type: 'address1'; payload: string | undefined }
  | { type: 'address2'; payload: string | undefined }
  | { type: 'email'; payload: string | undefined }
  | { type: 'reset' };
```

これも定義してあげると、Componentでintellisenseが働いて、Developing experienceが爆上がりします。


補足ですが、`payload`を`string | undefined`としているのは、React上`<input>`の初期値は`undefined`だからです。

# さいごに

`useState`と`useReducer`を上手く使いこなして、Reactを楽しく使いましょう！