ReactHookFormやっていく
react-hook-form
とyup
でフォームを作っていく
これを参考に型をつけていく
とりあえず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>()
useController
とuseWatch
はよくわかってない
他のライブラリを使って<input>
が隠蔽されてるときにController
のrender
でhook-formにコンポーネントを登録するって感じぽいかな
yup
でスキーマを定義してそれをinfer
で型定義に出来るのでそのままuseForm
のジェネリクスに食わせればformState
やerrors
に型定義として出てくる
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'
})
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 valuefalse
). If "null" is intended as an empty value be sure to mark the schema as.nullable()
.nullable()
つけろってことでとりあえず付けると大丈夫なんだけど本当にこれでいいのかよくわからない
schema
で配列を作るとuseFormContext
とかで取得したerrors
の型がFieldError[]
として推論される
けど実際は配列じゃない素のFieldError
なので実行時に落ちる。不具合っぽい
とりあえず@hookform/error-message
のErrorMessage
に食わせて表示させるのが安全ぽい
<ErrorMessage
errors={errors}
name="favorites"
render={({ message }) => <p>{message}</p>}
/>
checkboxGroupみたいな配列のやつはuseForm
のdefaultValues
で[]
を指定してあげないと何もチェックされてないときにfalse
になってしまって型と違ってしまっている
yup
のdefault
も効いてないぽい?
残り
- select
- radio
- 必須でないフィールドに対応させる
select
は特に考えることなくregister
するだけで使えた
<select {...register(name)}>
<option></option>
</select>
radio
も同様
<input type="radio" {...register(name)} {...props} />
Yup
でobject
の配列を書いてみる
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
として取得できる
サジェストはしてくれないけどタイポや型が違うときはエラーを出してくれる(どうやってるんだろう)
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
にはname
とnote
は選べるけどage
は選べない
ちょっと理解が怪しいのでもう一度確認するけど動作的にはこれでいいっぽい
検証&作ったもののサンプルコード
最初はフォーム全体のスキーマを一箇所で管理するようにしていたが、そうすると複数種類のフォームを作ろうとしたときにコンポーネントの使い回しが出来ない
<TextInput<Form1 | Form2> ... >
と書くとname
はどちらかの型に属しているフィールドしかサジェストしてくれない(本来ほしいのは両方の型に共通して存在するフィールド名)
最終的にはシンプルにコンポーネントそれぞれがスキーマを持ちそのスキーマを組み合わせてフォームの型を作るようにした
これによりコンポーネントは自分自身のことだけに集中すればよくなる
コンポーネント同士の値によってバリデーションしたい場合はそれぞれのスキーマを統合した後に上書きすればいいと思う(未確認)