【React】大きなフォームの値管理

Reactでの状態管理は主にstateを用いて行います。
フォームにインプットされた値をstateで管理することで、リアルタイムにバリデーションを行ったりすることができます。

しかし大きなフォームとなってくると、stateの記述量がかなり大きくなっていきます。
巨大なフォームをクリーンに管理するには、いくつか方法があります。

## stateでフォームを管理する

以下はユーザー情報を入力するフォームの例です。
各フォームの値をstateで管理しています。


```tsx
const StateForm: React.FC = () => {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [zipCode, setZipCode] = useState("");
  const [country, setCountry] = useState("");
  const [city, setCity] = useState("");
  const [address, setAddress] = useState("");

  return (
    <div className="form">
      <Input label="name" onChange={(e: React.ChangeEvent<HTMLInputElement>) => setName(e.target.value)} />
      <Input label="email" onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)} />
      <Input label="zipCode" onChange={(e: React.ChangeEvent<HTMLInputElement>) => setZipCode(e.target.value)} />
      <Input label="country" onChange={(e: React.ChangeEvent<HTMLInputElement>) => setCountry(e.target.value)} />
      <Input label="city" onChange={(e: React.ChangeEvent<HTMLInputElement>) => setCity(e.target.value)} />
      <Input label="address" onChange={(e: React.ChangeEvent<HTMLInputElement>) => setAddress(e.target.value)} />
    </div>
  );
};
```

現実でこの方法でフォームの値管理をする際、フォームのバリデーションエラー等の状態を管理するため、
エラーの状態を格納するstateが必要になってきます。エラーメッセージもstateで持ったりすると・・・かなり大変です。


```tsx
const StateForm: React.FC = () => {
  const [name, setName] = useState("");
  const [nameIsError, setNameIsError] = useState(false)
  const [email, setEmail] = useState("");
  const [emailIsError, setEmailIsError] = useState(false)
  // 略

};
```



## stateを大きなオブジェクトとして管理する

先ほどの問題を解消するため、state自体を大きなオブジェクトにできないかと考えるようになります。
これは、更新関数をすこし工夫すれば、実現可能です。
useStateの更新関数は、stateを丸ごと上書きするため、スプレッド構文等を利用して、専用の更新関数を用意します。


```tsx
type FormState = {
  name: string;
  email: string;
  zipCode: string;
  country: string;
  city: string;
  address: string;
};

const initialFormState: FormState = {
  name: "",
  email: "",
  zipCode: "",
  country: "",
  city: "",
  address: "",
};

const ObjectStateForm: React.FC  = () => {
  const [formState, setFormState] = useState(initialFormState);
  const setState = (value: Partial<FormState>) => {
    setFormState({
      ...formState,
      ...value,
    });
  };
  const inputCallback = {
    name: (e: React.ChangeEvent<HTMLInputElement>) => setState({ name: e.target.value }),
    email: (e: React.ChangeEvent<HTMLInputElement>) => setState({ email: e.target.value }),
    zipCode: (e: React.ChangeEvent<HTMLInputElement>) => setState({ zipCode: e.target.value }),
    country: (e: React.ChangeEvent<HTMLInputElement>) => setState({ country: e.target.value }),
    city: (e: React.ChangeEvent<HTMLInputElement>) => setState({ city: e.target.value }),
    address: (e: React.ChangeEvent<HTMLInputElement>) => setState({ address: e.target.value }),
  };

  return (
    <div className="form">
        <Input label="name" onChange={inputCallback.name} />
        <Input label="email" onChange={inputCallback.email} />
        <Input label="zipCode" onChange={inputCallback.zipCode} />
        <Input label="country" onChange={inputCallback.country} />
        <Input label="city" onChange={inputCallback.country} />
        <Input label="address" onChange={inputCallback.address} />
    </div>
  );
};
```

エラー等も管理する場合、同様の方法でstateを管理できるので先ほどに比べてかなり簡潔になりました。
値の更新のみであれば、この方法でも問題なく行えます。



## useReducerでフォームの値を管理

useReducerを用いて、同様の管理を行えます。エラーと値を同じオブジェクトで管理します。
Actionのtypeやreducerを別ファイルで管理します。

```ts
type FormValue = {
  name: string;
  email: string;
  zipCode: string;
  country: string;
  city: string;
  address: string;
};

type ErrorValue = {
  [key in keyof FormValue]?: boolean;
};

export type FormState = {
  value: FormValue;
  error: ErrorValue;
};

type Action =
  | {
      type: "update";
      value: Partial<FormValue>;
    }
  | {
      type: "error";
      error: Partial<ErrorValue>;
    };

export const initialFormState: FormState = {
  value: {
    name: "",
    email: "",
    zipCode: "",
    country: "",
    city: "",
    address: "",
  },
  error: {
    name: false,
    email: false,
    zipCode: false,
    country: false,
    city: false,
    address: false,
  },
};

// reducer
// stateとactionを受け取ってstateを返す純粋な関数
export const reducer = (state: FormState, action: Action): FormState => {
  switch (action.type) {
    case "update": {
      return {
        ...state,
        value: {
          ...state.value,
          ...action.value,
        },
      };
    }
    case "error": {
      return {
        ...state,
        error: {
          ...state.error,
          ...action.error,
        },
      };
    }
  }
};
```


```tsx
const ReducerForm: React.FC = () => {
  const [formState, dispatch] = useReducer(reducer, initialFormState);
  const handleInput = {
    name: (e: React.ChangeEvent<HTMLInputElement>) => dispatch({ type: "update", value: { name: e.target.value } }),
    email: (e: React.ChangeEvent<HTMLInputElement>) => dispatch({ type: "update", value: { email: e.target.value } }),
    zipCode: (e: React.ChangeEvent<HTMLInputElement>) => dispatch({ type: "update", value: { zipCode: e.target.value } }),
    country: (e: React.ChangeEvent<HTMLInputElement>) => dispatch({ type: "update", value: { country: e.target.value } }),
    city: (e: React.ChangeEvent<HTMLInputElement>) => dispatch({ type: "update", value: { city: e.target.value } }),
    address: (e: React.ChangeEvent<HTMLInputElement>) => dispatch({ type: "update", value: { address: e.target.value } })
  };

  return (
    <div className="form">
      <Input label="name" onChange={handleInput.name} />
      <Input label="email" onChange={handleInput.email} />
      <Input label="zipCode" onChange={handleInput.zipCode} />
      <Input label="country" onChange={handleInput.country} />
      <Input label="city" onChange={handleInput.city} />
      <Input label="address" onChange={handleInput.address} />
    </div>
  );
};
```

この方法で値管理をする利点は、[公式リファレンス](https://ja.reactjs.org/docs/hooks-reference.html#usereducer)に記述されている通りです。

> 通常、useReducer が useState より好ましいのは、複数の値にまたがる複雑な state ロジックがある場合や、前の state に基づいて次の state を決める必要がある場合です。また、useReducer を使えばコールバックの代わりに dispatch を下位コンポーネントに渡せるようになるため、複数階層にまたがって更新を発生させるようなコンポーネントではパフォーマンスの最適化にもなります。

複数stateを参照してstateを更新する場合、パフォーマンス部分を気にする場合に利点があるとのこと。

私が身近に恩恵を感じるのは、期待する動作が増えた場合だと思います。
フォームをクリアする、他のフォームの値を参照して値を埋める・・・等の実装をView(コンポーネント)側に記述しなくてよいというのは大きな利点です。
また、stateのオブジェクトの階層が増えてきた場合も活躍します。