- react-hook-form+zodとの比較
- react-hook-formは
useController
+ 個別のコンポーネント切り出し- 親から
name
とcontrol
を渡して、子でuseController({ name, control })
する
- 親から
- validatorはzod
Docs: https://tanstack.com/form/latest/docs/overview
GitHub: https://github.com/tanstack/form
- 最新版はv0.13.5
- React, Vue, Solid対応
- zod,yup,valibotのアダプタが提供されている
- number型のフィールド、useController使うとvalueAsNumber使えない
.transform
が生えたスキーマを使った時のuseWatch
,handleSubmit
あたりの型不整合useForm.resolver
でスキーマが確定するので、動的にフィールドが変わるフォームはunion/discriminatedUnion/refine/superRefineで頑張ってる
https://tanstack.com/form/latest/docs/framework/react/examples/zod
起点はuseForm
戻り値のform
からほぼ全てのapiが生えている(rhfのcontrolみたいな感じ)
const form = useForm({
defaultValues: {
firstName: '',
lastName: '',
},
onSubmit: async ({ value }) => {
// Do something with form data
console.log(value)
},
// Add a validator to support Zod usage in Form and Field
validatorAdapter: zodValidator,
})
formOptions.validators.onChange
にzodのスキーマを渡せばform levelでバリデーションができるっぽいが現状まだちゃんと実装されていない
form.Field
のrenderPropでFieldApi
がもらえるので、そこに生えているhandleChange
, handleBlur
とかvalue
を渡せばOK
<form.Field
name="firstName"
validators={{
onChange: z
.string()
.min(3, "First name must be at least 3 characters"),
onChangeAsyncDebounceMs: 500,
onChangeAsync: z.string().refine(
async (value) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return !value.includes("error");
},
{
message: "No 'error' allowed in first name",
},
),
}}
children={(field) => {
// Avoid hasty abstractions. Render props are great!
return (
<>
{/** ここをMUIに差し替えればOK */}
<label htmlFor={field.name}>First Name:</label>
<input
id={field.name}
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
/>
<FieldInfo field={field} />
</>
);
}}
/>
react-hook-formのcontrol
を受け取る代わりにfield
を受け取ってゴニョゴニョ、みたいな感じになると思われる
validatorまで共通化する場合はformとnameを受け取る感じか
form.Subscribe
はうれしい
react-hook-formでフォームにエラーがある場合にボタンを押せなくする、みたいなことをする際、フォームの最上位でconst { formState: { isValid } } = useController({ name, control })
すると、フォームのonChangeで毎回コンポーネントの再レンダリングが走ってしまう
なので重いフォームについてはこうやってコンポーネントを分けてformStateを監視し、フォーム側から使っていた
const SubmitButton: FC<Props> = ({ control }) = {
const { isValid } = useFormState({ control });
return <Button type="submit" disabled={!isValid}>Submit</Button>
}
form.Subscribe
を使うと
<form.Subscribe selector={(state) => [state.canSubmit]>
{([canSubmit]) => (
<Button type="submit" disabled={!canSubmit}>Submit</Button>
)}
</form.Subscribe>
その他
- number型のフィールド辛い問題についてはそもそも
valueAsNumber
みたいなヘルパも(今のところ)用意されていないので同じつらさはあると思う .transform
で型が変わる事象については、もしかしたらonSubmitのスキーマだけBaseSchema.transform()
でうまいこと推論してくれるようになる可能性はあるかもしれない- フィールドレベルで
validator
が指定できるので、動的にフィールドが変わるスキーマについてはやりやすいかと思った- その場合はフィールドで使われうるSchemaのみ共通化する感じになる?
- 個人的には最初にでかいスキーマを定義する方が好みなので、formレベルでのバリデーションでもうまい具合に動くようになることを期待
- 当たり前だけどプロダクションで使うには機能が足りていない v1のalphaが出たくらいでまた触ろうかなという気持ち
- form levelでのバリデーションに未対応とか型周りの不整合だったり ドキュメントもまだまだなので挙動について詳しく知るには実装を読むしかない
- zod周りの使い勝手は今のところどっちもどっちな気がする
- そもそもzodがフォームバリデータとしてどうなのか説はある yup,valibotも試してみたい気持ちになった
- onChangeAsync, onChangeAsyncDebounceMsみたいなプロパティが生えてるので、非同期のバリデーションについてはこっちの方が書きやすいかもしれない
- rhfに比べてコードが読みやすかった + まだあまり実装が大きくない + 機能も足りてないのでコントリビューションするには良いかも
- FieldApi.handleChange
- FieldApi.setValue
- FieldApi.validate
- FormApi.validate
- FormApi.validateSync
- FormApi.validateAsync
- FieldApi.validateSync
- FieldApi.validateAsync
- FormApi.validate
- FieldApi.setFieldValue
- FieldApi.validate
- FieldApi.setValue