Closed15

ReactHookFormやっていく

mistolteenmistolteen

https://react-hook-form.com/api

とりあえずuseFormでフォーム全体の設定をして<FormProvider>で囲う
囲われたコンポーネントの中ではuseFormContextでフォームの状態にアクセスしたりフォームに値をセットできるぽい
modeはバリデーションするタイミング。デフォルトはsubmitしたときになってるのでフィールドを離れた時用にonBlurにする

export const MyForm: React.VFC = () => {
  const methods = useForm<FormData>({
    resolver: yupResolver(schema),
    mode: 'onBlur'
  })
  const handleSubmit = methods.handleSubmit((data) => {
    console.log(data)
  })

  return (
    <FormProvider {...methods}>
      <form onSubmit={handleSubmit}>
        <TextInput<FormData> name="username" />
        <input type="submit" />
      </form>
    </FormProvider>
  )
}

const {
  setValue,
  formState: { errors, touchedFields }
} = useFormContext<FormData>()

useControlleruseWatchはよくわかってない

mistolteenmistolteen

yupでスキーマを定義してそれをinferで型定義に出来るのでそのままuseFormのジェネリクスに食わせればformStateerrorsに型定義として出てくる
yupResolverにスキーマを食わせてuseFormの引数に与えてあげればバリデーションもやってくれる
スキーマがそのままバリデーションになるのでいい感じ

const schema = Yup.object({
  firstName: Yup.string()
    .trim()
    .required('名を入力してください')
    .max(30, '名は30文字以内で入力してください')
})

export type FormData = Yup.InferType<typeof schema>
// type FormData = {
//   firstName: string;
// }

export const resolver = yupResolver(schema)
const methods = useForm<FormData>({
  resolver,
  mode: 'onBlur'
})

mistolteenmistolteen

checkboxGroupを作ろうとして↓みたいなスキーマを作ると、1個もチェックしてないときにバリデーションした際にcastエラーになる

Yup.array(Yup.string().required())
    .default([])
    .required()
    .min(1)

must be a array type, but the final value was: null (cast from the value false). If "null" is intended as an empty value be sure to mark the schema as .nullable()

.nullable()つけろってことでとりあえず付けると大丈夫なんだけど本当にこれでいいのかよくわからない

mistolteenmistolteen

schemaで配列を作るとuseFormContextとかで取得したerrorsの型がFieldError[]として推論される
けど実際は配列じゃない素のFieldErrorなので実行時に落ちる。不具合っぽい
とりあえず@hookform/error-messageErrorMessageに食わせて表示させるのが安全ぽい

<ErrorMessage
  errors={errors}
  name="favorites"
  render={({ message }) => <p>{message}</p>}
/>
mistolteenmistolteen

checkboxGroupみたいな配列のやつはuseFormdefaultValues[]を指定してあげないと何もチェックされてないときにfalseになってしまって型と違ってしまっている
yupdefaultも効いてないぽい?

mistolteenmistolteen

selectは特に考えることなくregisterするだけで使えた

<select {...register(name)}>
  <option></option>
</select>
mistolteenmistolteen

Yupobjectの配列を書いてみる

const schema = Yup.object({
  families: Yup.array().of(
    Yup.object({
      name: Yup.string().required(),
      age: Yup.number().required()
    })
  )
})
{[1, 2, 3].map((value, index) => {
  return (
    <React.Fragment key={index}>
      <label>家族{value}</label>
      <div>
        <label>名前</label>
        <TextInput<FormData> name={`families.${index}.name`} />
      </div>

      <div>
        <label>年齢</label>
        <NumberInput<FormData> name={`families.${index}.age`} />
      </div>
    </React.Fragment>
  )
})}

これで問題なくArrayとして取得できる
サジェストはしてくれないけどタイポや型が違うときはエラーを出してくれる(どうやってるんだろう)

mistolteenmistolteen
const schema = Yup.object({
  name: Yup.string().required(),
  note: Yup.string(),
  age: Yup.number()
})
export interface TextInputProps<T>
  extends Omit<InputHTMLAttributes<HTMLInputElement>, 'name'> {
  name: FieldByType<T, string | undefined>
}

任意入力のstringを定義した場合に対応するにはname: FieldByType<T, string | undefined>とする
TextInputにはnamenoteは選べるけどageは選べない
ちょっと理解が怪しいのでもう一度確認するけど動作的にはこれでいいっぽい

mistolteenmistolteen

最初はフォーム全体のスキーマを一箇所で管理するようにしていたが、そうすると複数種類のフォームを作ろうとしたときにコンポーネントの使い回しが出来ない
<TextInput<Form1 | Form2> ... >と書くとnameはどちらかの型に属しているフィールドしかサジェストしてくれない(本来ほしいのは両方の型に共通して存在するフィールド名)

最終的にはシンプルにコンポーネントそれぞれがスキーマを持ちそのスキーマを組み合わせてフォームの型を作るようにした
これによりコンポーネントは自分自身のことだけに集中すればよくなる

コンポーネント同士の値によってバリデーションしたい場合はそれぞれのスキーマを統合した後に上書きすればいいと思う(未確認)

このスクラップは2022/01/23にクローズされました