技術メモ

プログラミングとか電子工作とか

React Hook Form入門 その2(基本編)

f:id:ysmn_deus:20200224171811p:plain

前回の続きです。

基本的な使い方

バリデーションなどを無視するなら、基本的にはフォームの要素のref属性にuseFormから得られるregister関数を登録し、formのonSubmithandleSubmitを登録するだけで利用できます。
あえてフォームを作る順序を書くなら

  1. フォームの要素を書き出す
  2. フィールドを登録する
  3. スタイリング(ここでは取り扱わない)

という流れになるかと思います。最初から順にやっていきます。

フォームの要素を書き出す

基本的には普通のHTMLとなんら変わり無い要素をReactで表現します。

import React from "react"

export default function App() {
  return (
    <form>
      <input name="firstName" />
      <select name="gender">
        <option value="male">male</option>
        <option value="female">female</option>
      </select>
      <input type="submit" />
    </form>
  )
}

f:id:ysmn_deus:20200224173934p:plain

非常にシンプルなフォームですが、今回はこれに機能を実装していきます。

フィールドを登録する

ここでは

  • input(name)
  • select(gender)

という2種類のフォームの要素があります。これらをReact Hook Formのフィールドに登録していきます。

import React from "react"
import { useForm } from "react-hook-form" // useFormをreact-hook-formからインポート

export default function App() {
  const { register, handleSubmit } = useForm() // register, handleSubmitをuseFormというHooksから得る
  const onSubmit = (data: any) => console.log(data) // フォームが送信されたときに実行される関数

  return (
    // form要素にhandleSubmitを渡す。なお、引数にはフォーム送信時に実行される関数を渡す。
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* input 要素にref属性を付与、registerを渡す*/}
      <input name="firstName" ref={register} />
      {/* select 要素にref属性を付与、registerを渡す*/}
      <select name="gender" ref={register}>
        <option value="male">male</option>
        <option value="female">female</option>
      </select>
      <input type="submit" />
    </form>
  )
}

基本的にはこれだけでフォームが利用可能です。
フォームのデータを利用してなにがしかしたいときはonSubmitの中に処理を書いていきます。

心がTSに支配されてる方は(data: any)で発狂しそうかと思いますが、後で言及するので我慢して下さい。僕も発狂寸前なんです。

非同期処理

おそらくフォームの送信の後にはAPIと通信したりするので非同期処理が来ると思います。
上記のhandleSubmitの引数にはPromiseも渡せるので、async関数がそのまま渡せます。

import React from "react"
import { useForm } from "react-hook-form"

const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))

export default function App() {
  const { register, handleSubmit } = useForm()
  const onSubmit = async (data: any) => {
    console.log(data)
    await sleep(1000)
    console.log("complete.")
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="firstName" ref={register} />
      <select name="gender" ref={register}>
        <option value="male">male</option>
        <option value="female">female</option>
      </select>
      <input type="submit" />
    </form>
  )
}

バリデーションを適応する

基本的には上記までで良さそうなものですが、バリデーションぐらいはフロントでもやっておくれ、という気持ちが湧いてきます。
React Hooks FormはHTML 標準のフォームバリデーションのバリデーションには対応しているようです。
対応しているルールは公式サイトを閲覧してもらえればと思いますが、大雑把に言えば

などのルールを適応し、バリデーションに通らない場合は即座にエラーを得ることができるようです。

import React from "react"
import { useForm } from "react-hook-form"

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="firstName" ref={register({ required: true })} />
      {errors.firstName && "First name is required"}
      <input name="lastName" ref={register({ pattern: /^[A-Za-z]+$/i })} />
      {errors.lastName && "Last name is not mached [A-Za-z]"}
      <input name="age" type="number" ref={register({ min: 18, max: 99 })} />
      {errors.age && "age is in ragnge 18-99"}
      <input type="submit" />
    </form>
  )
}

errors.の後にname属性で決めたプロパティを参照するとバリデーションのルールを適応した可否が得られます。
上記の例ではboolean &&の形で、もしバリデーションが通ってなければ警告文を表示する様な形式にしています。
バリデーションに適合していなければ、送信してもonSubmitが実行されません。

UIライブラリの利用

一応方法としては

  1. 対応するUIライブラリ(material-uiなど)を利用する
  2. Controllerを利用してカスタム登録処理をする
  3. useEffectを利用する

の3種類ありますが、自分は基本的にmaterial-uiしか利用しないので上記2点にのみ言及します。

対応するUIライブラリを利用する

これが最も簡単です。material-uiを利用している人は、属性を追加するだけで利用できます。
まずはmaterial-uiをインストールします。

yarn add @material-ui/core

もうこれでフォームが作成可能です。
ソースを変更していきます。

import React from "react"
import { useForm } from "react-hook-form"
import TextField from "@material-ui/core/TextField"

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <TextField inputRef={register} label="First name" name="firstName" />
      <input type="submit" />
    </form>
  )
}

これで、TextFieldコンポーネントに入力された値がonSubmitdataの中に放り込まれます。

Controllerを利用してカスタム登録処理をする

テキストフィールドやチェックボックスぐらいであれば上記の処理で問題無いんですが、ラジオボタンなどのフォーム要素が制御されたもの等を利用する場合は上記の用にはいきません。
例えば、ラジオボタンの例を出せば

import React, { useState } from "react"
import { useForm } from "react-hook-form"
import {
  TextField,
  FormControl,
  FormLabel,
  FormControlLabel,
  Radio,
  RadioGroup,
} from "@material-ui/core"

export default function App() {
  const { register, handleSubmit } = useForm()
  const onSubmit = (data: any) => console.log(data)
  const radioList: string[] = ["male", "female", "other"]
  const [value, setValue] = useState(radioList[0])
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setValue((event.target as HTMLInputElement).value)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <TextField inputRef={register} label="First name" name="firstName" />
      <FormControl>
        <FormLabel>Gender</FormLabel>
        <RadioGroup name="gender1" value={value} onChange={handleChange}>
          {radioList.map(item => (
            <FormControlLabel
              key={item}
              value={item}
              control={<Radio />}
              label={item}
            />
          ))}
        </RadioGroup>
      </FormControl>
      <input type="submit" />
    </form>
  )
}

といった場合にはinputRefregisterを登録すればOKという訳にもいきません。
そこで、React Hook FormにはControllerというものが用意されています。

import React from "react"
import { useForm, Controller } from "react-hook-form"
import {
  TextField,
  FormLabel,
  FormControlLabel,
  Radio,
  RadioGroup,
} from "@material-ui/core"

const radioList: string[] = ["male", "female", "other"]

export default function App() {
  const { register, handleSubmit, control } = useForm()
  const onSubmit = (data: any) => console.log(data)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <TextField inputRef={register} label="First name" name="firstName" />
      {/* 上記と同じ、Controllerを利用してTextFieldを使い場合の例 */}
      <Controller
        name="firstName2"
        as={TextField}
        control={control}
        defaultValue=""
      />
      {/* FormControlのセクションを<div></div>と想定 */}
      <div>
        <FormLabel>Gender</FormLabel>
        <Controller
          name="radioGroup"
          as={
            <RadioGroup name="gender1">
              {radioList.map(item => (
                <FormControlLabel
                  key={item}
                  value={item}
                  control={<Radio />}
                  label={item}
                />
              ))}
            </RadioGroup>
          }
          control={control}
          defaultValue={radioList[0]}
        />
      </div>

      <input type="submit" />
    </form>
  )
}

やや複雑になりましたが、<FormControl></FormControl>の例と比べても、そこまで記述量が増えていないにもかかわらずフォームデータのやりとりができるのが確認できます。
なお、ここではしれっとdefaultValueを指定していますが、こいつを指定していないとMaterial-UIはコンパイル時にエラーを吐きます。
(たぶんa component is changing an uncontrolled radiogroup to be controlled react hook formみたいなやつ)
フォームの要素を記述する時にdefaultValue={}として値を指定するか、useFormの歳に引数に初期値を入力するかのどちらかで対応して下さい。

TypeScriptの対応

上記までは、onSubmitにany型をたたき込んだりとTypeScriptの良さを出し切れてない使い方でしたが、useFormに型を適応することでuseFormから得られるsetValueerrorsに型を適応することができます。

import * as React from "react"
import { useForm } from "react-hook-form"

// フォームにあるフィールドの型の定義
type FormData = {
  firstName: string
  lastName: string
}

export default function App() {
  const { register, setValue, handleSubmit, errors } = useForm<FormData>() // フィールドの型定義をuseFormに適応する
  // handleSubmitの引数にはフィールドの型定義が適応されている
  const onSubmit = handleSubmit(({ firstName, lastName }) => {
    console.log(firstName, lastName)
  })

  return (
    <form onSubmit={onSubmit}>
      <label>First Name</label>
      <input name="firstName" ref={register} />
      <label>Last Name</label>
      <input name="lastName" ref={register} />
      <button
        type="button"
        onClick={() => {
          // setValueにも型定義が適応されている
          setValue("lastName", "luo") // OK
          setValue("firstName", true) // コンパイルエラー
          errors.bill // errorsにも型定義が適応されているのでコンパイルエラー
        }}
      >
        SetValue
      </button>
    </form>
  )
}