はじめに
みなさん、フォームライブラリは何を使っていますか?
React Hook Formがフォームの一時代を築いた後に、server actionに対応しているConformが登場したり、様々なフォームライブラリがあると思いますが、今回は最近v1になったTanStack Formについてご紹介します。
TanStack Formは、各入力フィールドを独立したコンポーネントとして提供します。これにより、フォーム全体を再描画するのではなく、変更が生じた特定のフィールドのみを選択的に再レンダリングすることが可能となり、パフォーマンスの最適化を実現しています。
TanStack Formの推しポイント👍
まずは、TanStack Formの推しポイントをいくつかご紹介します。
強力な型サポート
TanStack系のライブラリ全般の特徴になりますが、TypeScriptでできているため型安全・型補完が効くため開発体験がとても良いです。
様々なスキーマライブラリに対応
以下の様々なスキーマライブラリに対応しているのも魅力の一つです。複数のスキーマライブラリ候補から選択できることは嬉しいポイントですね!(最近、ArkTypeも流行っていますし…!!)
非同期バリデーション
これが私が一番感動した激推しポイントになります。以下のように実装するだけで非同期バリデーションが可能になるんです!!
form.Field
name="age"
asyncDebounceMs={500}
validators={{
onBlurAsync: async ({ value, signal }) => {
const currentAge = await fetchCurrentAgeOnProfile({ signal })
return value currentAge ? 'You can only increase the age' : undefined
},
}}
/>
React Hook Formなどでは非同期バリデーションを行うために、debounceを一から実装する必要がありました。TanStack Formではバリデーションのタイミングをコントロールしやすいように、asyncDebounceMs
などのプロパティが存在し、わざわざdebounceを実装しなくても大丈夫なのです!!
様々なフレームワーク/ライブラリをサポート
TanStack Formは、様々なフレームワーク/ライブラリで動作するように別々のパッケージが提供されています。各フレームワーク/ライブラリ間で一部機能が異なりますが、コアのAPIは共通であり、フレームワーク/ライブラリに影響されずにTanStack Formを選定することができます。
サポートされているフレームワークは以下の通りです。(ReactNativeにも対応)
Next.jsのserver actionに対応
現在、Next.jsのserver actionに対応している主要なフォームライブラリはConformであり、server actionを使用する場合はConformの一択かなと考えていました。しかし、TanStack FormもNext.jsのserver actionに対応しており、有力な選択肢として上がってくるのではないでしょうか。
React Hook Formとの違い
状態管理の違い
React Hook FormはReactのStateでデータを持っているのに対し、TanStack FormはObserverパターンでDOMで値を管理します。そのため、React以外のライブラリでも使用することができるようになっているようです。
インストールサイズが大きい
残念ながら、TanStack FormはReact Hook Formと比べてインストールサイズは大きいです。これはTanStack系のライブラリは強力な型サポートがある分、それがインストールサイズに直結しているようです。
ただ、バンドルサイズとしては問題がないようです。
他フォームライブラリの比較についても公式がまとめているので、気になれば見てみてください!
実装編
TanStack Formを使って簡単なログインフォームを作成しました。コードの全体は以下です。
import { useForm } from "@tanstack/react-form";
import React from "react";
import { loginSchema } from "./schema/login";
export const Form: React.FC = () => {
const form = useForm({
defaultValues: {
email: "",
password: "",
},
onSubmit: ({ value }) => {
console.log(value);
},
validators: {
onChangeAsync: loginSchema,
onChangeAsyncDebounceMs: 500,
},
});
const handleSubmit = (e: React.FormEventHTMLFormElement>) => {
e.preventDefault();
form.handleSubmit();
};
return (
form onSubmit={handleSubmit}>
{(field) => (
div>
label htmlFor={field.name}>Emaillabel>
input
type="email"
id={field.name}
name={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.isTouched &&
field.state.meta.errors.map((error) => {
return p>{error?.message}p>;
})}
div>
)}
/>
{(field) => (
div>
label htmlFor={field.name}>passwordlabel>
input
type="password"
id={field.name}
name={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.isTouched &&
field.state.meta.errors.map((error) => {
return p>{error?.message}p>;
})}
div>
)}
/>
form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
button type="submit" disabled={!canSubmit}>
{isSubmitting ? "Submitting..." : "Submit"}
button>
)}
/>
form>
);
};
useForm()
について
const form = useForm({
defaultValues: {
email: "",
password: "",
},
onSubmit: ({ value }) => {
console.log(value);
},
validators: {
onChangeAsync: loginSchema,
onChangeAsyncDebounceMs: 500,
},
});
useForm()
は以下で説明するdefaultValues
等をインプットをもとに、form
インスタンスを作成します。
defaultValues
defaultValues
は各フィールドの初期値を設定します。この例では、email
とpassword
というフィールドを初期化し、空の文字列を設定しています。
onSubmit
onSubmit
はサブミットされたときに実行される関数です。今回はコンソールを出力していますが、実際ではAPIリクエスト等の処理になります。
validators
validators
はどのタイミングでバリデーションを行うかを設定します。
今回は非同期バリデーションを試したかったのでonChange
の非同期版であるonChangeAsync
を使用しています。またonChangeAsyncDebounceMs
を設定することで500msでdebounceしてバリデーションされます。
今回使用したプロパティ以外も以下のようなプロパティがあります。
onBlur
onBlurAsync
onBlurAsyncDebounceMs
onMount
onSubmit
onSubmitAsync
について
form.Field
name="email"
children={(field) => (
div>
label htmlFor={field.name}>Emaillabel>
input
type="email"
id={field.name}
name={field.name}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
{field.state.meta.isTouched &&
field.state.meta.errors.map((error) => {
return p>{error?.message}p>;
})}
div>
)}
/>
はテキストやチェックボックスなどの各入力フィールドを定義します。
今回はchildrenとして入力フォーム要素を受け取り、レンダリングしています。またfield
からname
やstate
を取得し各プロパティに設定します。
フィールドのアクセス状態について
各フィールドのアクセス状態についてはfield.state
の以下の値を参照します。
種別 | アクセス状態 |
---|---|
isTouched |
ユーザーがフィールドにタップ、タッチした後 |
isPristine |
ユーザーが値を変更するまで |
isDirty |
ユーザーが値を変更した後 |
について
form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit, isSubmitting]) => (
button type="submit" disabled={!canSubmit}>
{isSubmitting ? "Submitting..." : "Submit"}
button>
)}
/>
は、フォームの状態をsubscribeするコンポーネントです。
サブミット可能かどうかはcanSubmit
で, サブミット中かどうかはisSubmitting
をsubscribeし、ボタンの状態を制御しています。
はコンポーネントレベルの再レンダリングを引き起こしません。subscribeしている値が変更されるたびに、
内の要素のみが再レンダリングされます。これにより、フォーム全体が再レンダリングされることなく、必要な部分だけを効率的に更新できるため、大規模なフォームでも高いパフォーマンスを維持できます。
以下のドキュメントを参照してください。
まとめ
TanStack Formは型安全で開発体験もよく、またシンプルでとても良いフォームライブラリだなと感じました。React Hook FormやConformではなくTanStack Formをどんどん使っていくのもありだなと思いました。また、TanStack Formには多様な機能が揃っているので、より深いレベルまで使いこなして、フォーム実装の可能性を最大限に引き出していきたいです!
個人的にTanStack系のライブラリは、色々と気になっているのでまた気に入ったライブラリがあればシェアできたらなと思います。
最後までお読みいただき、ありがとうございました!!