React Hook Form + Yupを使って複数画面にまたがるフォームを作成する

## React Hook Form
https://react-hook-form.com/

フォームの作成・バリデーションが簡単にできる便利なライブラリです。
Uncontrolled Components（非制御コンポーネント）を採用しており、stateの変更に伴う再レンダリングの数を減らし、アプリのパフォーマンスを向上させることができます。

#### 制御コンポーネント
入力値をstateなどで保持するように制御されたもの

#### 非制御コンポーネント
HTMLのinputやtextareaのように、入力値をDOM自身が保持しているもの


## Yup
https://github.com/jquense/yup

バリデーションのためのライブラリです。
JavaScriptで スキーマを定義して値の解析・検証・変換などを行うことができます。

 React Hook Formだけでもバリデーションを行うことは可能ですが、Yupを使用すればバリデーションに関する記述を一箇所にまとめたり、より複雑で表現力のあるバリデーションを行うことができます。
 React Hook Formが標準でサポートしていて導入も簡単なので、今回はこちらを使用したいと思います。

## やりたいこと
入力が2画面、確認用を1画面用意。
値を検証して問題がなければ、入力した値をまとめて送信

## 使い方
### React Hook Formの基本的な使用方法
以下は公式から拝借したコードです。
registerを使用してinputなどの要素を登録し、検証ルールをReact Hook Formに適用することができます。
```Javascript
import { useForm } from "react-hook-form";

export default function App() {
  const { register, handleSubmit, watch, formState: { errors } } = useForm();
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input defaultValue="test" {...register("example")} />
      <input {...register("exampleRequired", { required: true })} />

      {errors.exampleRequired && <span>This field is required</span>}
      
      <input type="submit" />
    </form>
  );
}

```


registerを呼び出すことで、下記の参照をまとめて行うことができます

```Javascript
const { onChange, onBlur, name, ref } = register('firstName'); 
        
<input 
  onChange={onChange}
  onBlur={onBlur}
  name={name}
  ref={ref}
/>

// 同上
<input {...register('firstName')} />
```
Registerを使用したバリデーションは下記のように行うことができます。

```Javascript
<input
  {...register("test", {
    required: true
  })}
/>

<input
  {...register("test", {
    pattern: /[A-Za-z]{3}/
  })}
/>
```

簡単な使い方がわかったところで、実装していきましょう！

## いざ実装

### Yupを使ってスキーマを定義する
objectにスキーマを定義していきます。
```Javascript:src/utils/schema.ts
import { object, string, number } from 'yup';

const Schema = object({
  name: string().required('お名前の入力は必須です。'),
  age: number().typeError('年齢は数字を入力してください').integer('整数を入力してください'),
  email: string().email('メールアドレスの形式で入力してください。').required('メールアドレスの入力は必須です。'),
  phone: string().matches(/^[0-9]*$/, { message: '半角数字で入力してください。' }),
  favorite: string(),
});

export type SchemaType = InferType<typeof Schema>;
```
エラー時の文言は、それぞれの引数に設定しています。
スキーマはインターフェースを生成します。InferTypeはそのインターフェイスを抽出し、型生成を行ってくれます。

### 親ページの作成
今回はNextjsを利用しているため、pages配下に親となるページを作成します。

```Javascript:src/pages/form.tsx
import { yupResolver } from '@hookform/resolvers/yup';
import { Container, Typography } from '@mui/material';
import type { NextPage } from 'next'
import { useState } from 'react';
import { useForm, FormProvider } from 'react-hook-form';
import Form1 from '../components/form1';
import Form2 from '../components/form2';
import Confirm from '../components/confirm';
import { Schema, SchemaType } from '../utils/schema';

const Form: NextPage = () => {
  const methods = useForm<SchemaType>({
    resolver: yupResolver(Schema),
    mode: 'onBlur',
  });
  const [pageNumber, setPageNumber] = useState(0);

  const onNext = () => {
    setPageNumber(state => state + 1);
  };

  const onPrev = () => {
    setPageNumber(state => state - 1);
  };

  return (
    <>
      <Container maxWidth="md" sx={{ marginTop: 7 }}>
        <Typography variant="h3" gutterBottom component="div">
          フォーム
        </Typography>
        <FormProvider {...methods}>
          <form>
            {pageNumber === 0 && <Form1 onNext={onNext} /> }
            {pageNumber === 1 && <Form2 onNext={onNext} onPrev={onPrev} /> }
            {pageNumber === 2 && <Confirm onPrev={onPrev} /> }
          </form>
        </FormProvider>
      </Container>
    </>
  )
}

export default Form;

```
ページ切り替えをuseStateで行ってますが、[Layouts](https://nextjs.org/docs/basic-features/layouts)とか他の方法でも問題ありません。

useFormのresolverを使用することでYup等、外部検証ライブラリを利用できます。
バリデーションはBlurのタイミングで行われるように設定しています。

```React
<FormProvider {...methods}>
  // 〜〜〜
</FormProvider>
```

FormProviderは、 ReactのContext APIに基づいて構築されています。
これを使うことで、propsのバケツリレーを回避することができます。


### 入力画面の作成
```React:src/components/form1.tsx
import { useFormContext } from "react-hook-form";
import { Box, Button, TextField } from '@mui/material';
import { Schema, SchemaType } from '../utils/schema';

type Props = {
  onNext: () => void
}

const Form1 = ({ onNext }: Props) => {
  const methods = useFormContext<SchemaType>();
  const {
    register,
    formState: { errors },
  } = methods;
  return (
    <>
      <TextField
        id="outlined-required"
        label="お名前"
        fullWidth
        {...register("name")}
        margin="normal"
        error={Boolean(errors.name)}
        helperText={errors.name?.message}
      />
      <TextField
        id="outlined-required"
        label="年齢"
        fullWidth
        margin="normal"
        {...register("age")}
        error={Boolean(errors.age)}
        helperText={errors.age?.message}
      />
      <TextField
        id="outlined-required"
        label="メールアドレス"
        fullWidth
        margin="normal"
        {...register("email")}
        error={Boolean(errors.email)}
        helperText={errors.email?.message}
      />
      <Box display="flex" justifyContent="center" gap={2} mt={2}>
        <Button variant="contained" onClick={onNext}>
          次へ
        </Button>
      </Box>
    </>
  )
}

export default Form1;

```
```React:src/components/form2.tsx
import { Schema, SchemaType } from '../utils/schema';
import { useFormContext } from "react-hook-form";
import { Box, Button, TextField } from '@mui/material';

type Props = {
  onNext: () => void
  onPrev: () => void
}

const Form2 = ({ onNext, onPrev }: Props) => {
  const methods = useFormContext<SchemaType>();
  const {
    register,
    formState: { errors },
  } = methods;
  return (
    <>
      <TextField
        label="電話番号"
        variant="outlined"
        fullWidth
        {...register("phone")}
        margin="normal"
        error={Boolean(errors.phone)}
        helperText={errors.phone?.message}
      />
      <TextField
        label="好きな食べ物"
        variant="outlined"
        fullWidth
        {...register("favorite")}
        error={Boolean(errors.favorite)}
        helperText={errors.favorite?.message}
        margin="normal"
      />
      <Box display="flex" justifyContent="center" gap={2} mt={2}>
        <Button variant="contained" onClick={onPrev}>
          戻る
        </Button>
        <Button variant="contained" onClick={onNext}>
          確認
        </Button>
      </Box>
    </>
  )
}

export default Form2;

```
useFormContextで、ネストされたコンポーネントからReact Hook Formへ接続します。
先程呼び出したuseFormのメソッドを子要素のコンポーネントから簡単に呼び出すことができます。
propsを受け取る必要がないのでラクですね！

見た目はこのような感じです。
1ページ目↓
![スクリーンショット 2022-11-29 11.27.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988346/38d1ff2d-b7e6-f496-4129-d6bf77038ef4.png)
2ページ目↓
![スクリーンショット 2022-11-29 11.27.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988346/f6f72533-3969-a9cc-2a78-98102cb9696c.png)




### MaterialUIとの連携
入力フォームのスタイリングにはMaterialUIを使用しています。
ちなみにMaterialUIは「制御コンポーネント」です。

React Hook Formには、このような外部制御されているコンポーネントと簡単に統合を行うためのラッパーコンポーネントも提供されています。
[Controller](https://react-hook-form.com/api/usecontroller/controller)

RadioボタンやCheckboxを利用するときにはControllerを使う必要があるのですが、
TextFieldであれば普通にregisterを使用しても問題ありません。

Radioボタンなどを使いたいときには、Controllerを利用して下記のように対応できます。
```React
<Controller
  name="fruit"
  control={control} 
  render={({ field }) => {
    return(
      <RadioGroup
        row
        { ...field }
      >
        <FormControlLabel value="apple" control={<Radio />} label="Apple" />
        <FormControlLabel value="orange" control={<Radio />} label="Orange" />
      </RadioGroup>
    );
  }}
/>
```

Controllerを使うのであれば、TextFieldもControllerで制御できるように記述方法を統一したほうがきれいかなーと思います。
しかしそうなると、いよいよ非制御コンポーネントの恩恵を受けられなくなってしまうのでは..！？って気もしますが、
React Hook Formは制御されたコンポーネントの再レンダリングも最適化をおこなってくれるようです。
[非制御コンポーネントと混合された制御](https://react-hook-form.com/advanced-usage#ControlledmixedwithUncontrolledComponents)


### 確認画面の実装
```React:src/components/confirm.tsx
import { useFormContext, SubmitHandler, SubmitErrorHandler } from "react-hook-form";

const Confirm = ({ onPrev }: Props) => {
  const methods = useFormContext<SchemaType>();
  const {
    handleSubmit,
    getValues,
    formState: { errors },
  } = methods;
  const [sendData, setSendData] = useState<SchemaType | null>(null);

  const onSubmit: SubmitHandler<SchemaType>  = data => {
    console.log('Success', data)
    setSendData(data)
  };
  
  const onErr: SubmitErrorHandler<SchemaType> = err => console.log('Error', err);

  useEffect(() => {
    handleSubmit(onSubmit, onErr)();
  }, [])

  return (
    <>
      <TableContainer>
        <Table>
          <TableBody>
            <TableRow>
              <TableCell component="th" scope="row">
                お名前
              </TableCell>
              <TableCell>
                {getValues('name')}
                <span style={{ color: 'red' }}>{errors.name?.message}</span>
              </TableCell>
            </TableRow>
            <TableRow>
              <TableCell component="th" scope="row">
                年齢
              </TableCell>
              <TableCell>
                {getValues('age')}
                <span style={{ color: 'red' }}>{errors.age?.message}</span>
              </TableCell>
            </TableRow>

            ~~繰り返す~~


          </TableBody>
        </Table>
      </TableContainer>

      <Box display="flex" justifyContent="center" gap={2} mt={2}>
        <Button variant="contained" onClick={onPrev}>
          戻る
        </Button>
        <Button
          variant="contained"
          onClick={() => alert(JSON.stringify(sendData))}
          disabled={!Boolean(sendData)}
        >
          送信
        </Button>
      </Box>
    </>
  )
}

export default Confirm;

```

値の表示は、getvaluesで行っています。
```React
{getValues('age')}
```

エラー時には下記のerrorsオブジェクトにエラーの内容が入るので、それを表示しています。
```React
formState: { errors },
```

表示はこのような感じです。
エラー時↓
![スクリーンショット 2022-11-29 13.53.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988346/a72f0c3b-254c-d6da-abc8-2c624e7b44c9.png)


OK時↓
![スクリーンショット 2022-11-29 11.32.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988346/4e400b17-36d7-dc8c-f021-679a0ecf3b76.png)


初回レンダリング時にhandleSubmit(onSubmit, onErr)()を実行し、すべての値を検証しています。
バリデーションに問題がなければonSubmitが実行され、エラーがあればonErrが実行されます。

onSubmitでdataをuseStateに格納します。
```React
  const onSubmit: SubmitHandler<SchemaType>  = data => {
    console.log('Success', data)
    setSendData(data)
  };
```

sendDataに値が入れば、送信ボタンが活性になります。
onClick内で送信すれば完了です。


## まとめ
基本的な機能のみの使用ですが、簡単にフォームが作成できました。
どちらのライブラリもシンプルで使いやすく拡張性もあるので、入力が多くてもっと規模の大きいフォームだと開発スピードの面、パフォーマンス面でもかなり恩恵を受けられそうです。

利用者も多くWeb上にも情報がたくさんある＆公式ドキュメントも見やすいので導入のハードルも低いところも良いと思います。