formik、react-final-form、react-hook-form、react-jsonschema-form、redux-form はすべて React アプリケーションにおけるフォーム状態管理とバリデーションを支援するライブラリです。これらのライブラリは、入力値の同期、エラーメッセージの表示、サブミット処理、フォーム再利用性といった共通課題に対処しますが、アーキテクチャやパフォーマンス特性、API設計において明確な違いがあります。特に、コンポーネント再レンダリングの頻度、外部状態管理との統合方法、および宣言的な記述スタイルの有無が選定の重要な判断材料となります。
React のフォーム実装は一見単純に見えますが、入力検証、エラーハンドリング、サブミット制御、動的フィールド管理など、実際のプロダクトでは多くの課題が伴います。formik、react-final-form、react-hook-form、react-jsonschema-form、redux-form はそれぞれ異なるアプローチでこれらの課題に取り組んでいますが、その設計思想とトレードオフを理解することが正しい選択につながります。
重要:
redux-formは npm ページおよび GitHub リポジトリで明確に非推奨(deprecated)とされています。新規プロジェクトでは絶対に使用せず、既存コードがあれば移行を検討してください。
formik: Context による内部状態管理formik は独自の Context を使ってフォーム状態を管理します。すべてのフィールドが controlled コンポーネントとして動作し、状態変更時にフォーム全体が再レンダリングされる傾向があります。
import { useFormik } from 'formik';
function MyForm() {
const formik = useFormik({
initialValues: { email: '' },
onSubmit: values => alert(JSON.stringify(values, null, 2)),
validate: values => {
const errors = {};
if (!values.email) errors.email = '必須です';
return errors;
}
});
return (
<form onSubmit={formik.handleSubmit}>
<input
name="email"
onChange={formik.handleChange}
value={formik.values.email}
/>
{formik.errors.email && <div>{formik.errors.email}</div>}
<button type="submit">送信</button>
</form>
);
}
react-final-form: 最小限の再レンダリングを目指す設計react-final-form は Field コンポーネント単位で再レンダリングを制御します。フォーム全体ではなく、実際に変更されたフィールドのみが更新されます。
import { Form, Field } from 'react-final-form';
const MyForm = () => (
<Form
onSubmit={values => alert(JSON.stringify(values, null, 2))}
validate={values => {
const errors = {};
if (!values.email) errors.email = '必須です';
return errors;
}}
render={({ handleSubmit, pristine, invalid }) => (
<form onSubmit={handleSubmit}>
<Field name="email">
{({ input, meta }) => (
<div>
<input {...input} placeholder="Email" />
{meta.error && meta.touched && <span>{meta.error}</span>}
</div>
)}
</Field>
<button type="submit" disabled={pristine || invalid}>
送信
</button>
</form>
)}
/>
);
react-hook-form: uncontrolled コンポーネント中心のアプローチreact-hook-form は React の uncontrolled コンポーネントを活用し、状態を ref で管理することで再レンダリングを極力回避します。パフォーマンスが非常に高く、特に大規模フォームや高頻度入力に有利です。
import { useForm } from 'react-hook-form';
function MyForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit(data => alert(JSON.stringify(data, null, 2)))}>
<input
{...register('email', {
required: '必須です'
})}
placeholder="Email"
/>
{errors.email && <p>{errors.email.message}</p>}
<button type="submit">送信</button>
</form>
);
}
react-jsonschema-form: JSON Schema からの自動生成このライブラリは JSON Schema 定義からフォームを自動生成します。状態管理は内部で行われますが、カスタムロジックの実装は制限されます。
import Form from 'react-jsonschema-form';
const schema = {
type: "object",
properties: {
email: { type: "string", title: "メールアドレス" }
},
required: ["email"]
};
function MyForm() {
return (
<Form
schema={schema}
onSubmit={({ formData }) => alert(JSON.stringify(formData, null, 2))}
/>
);
}
redux-form: Redux ストアへの状態統合(非推奨)redux-form はフォーム状態を Redux ストアに保存します。これにより、アプリケーション全体の状態とフォーム状態が密結合し、パフォーマンスやデバッグに悪影響を及ぼすことがありました。
// 非推奨のため、新規実装は避けてください
import { reduxForm, Field } from 'redux-form';
const renderField = ({ input, meta: { touched, error } }) => (
<div>
<input {...input} />
{touched && error && <span>{error}</span>}
</div>
);
let MyForm = (props) => (
<form onSubmit={props.handleSubmit(values => alert(JSON.stringify(values)))}>
<Field name="email" component={renderField} />
<button type="submit">送信</button>
</form>
);
MyForm = reduxForm({ form: 'myForm' })(MyForm);
formik: デフォルトではフォーム全体が再レンダリングされるため、useCallback や memo による最適化が必要な場合があります。<Field> コンポーネントを使うことで多少改善されますが、根本的な制約は残ります。react-final-form: 各 <Field> が独立して再レンダリングされるため、大規模フォームでも安定したパフォーマンスを維持できます。react-hook-form: uncontrolled コンポーネントを採用しているため、ユーザー入力時の再レンダリングが発生せず、最も高いパフォーマンスを実現します。react-jsonschema-form: 内部で再レンダリング最適化が行われていますが、カスタムフィールドを追加するとパフォーマンスが低下する可能性があります。redux-form: フォーム状態が Redux ストアに保存されるため、すべての状態変更でストア全体が更新され、パフォーマンス上の問題が頻発しました(非推奨の主な理由の一つ)。formik: カスタムバリデーションや動的フィールド追加が直感的に実装できますが、高度な制御(例:非同期バリデーションのキャンセル)には追加コードが必要です。react-final-form: フォームロジックを完全に分離できるため、複雑なビジネスロジックにも柔軟に対応できます。react-hook-form: カスタムバリデーションルールや動的フィールドもサポートしていますが、完全な uncontrolled 設計のため、一部のユースケース(例:他のフィールド値に依存した動的表示)では useWatch などの追加フックが必要です。react-jsonschema-form: UI スキーマでレイアウトを調整できますが、特殊なインタラクション(例:ドラッグ&ドロップによるフィールド並び替え)は難しいです。redux-form: Redux ミドルウェアとの連携は可能でしたが、フォームロジックがストアに分散するため保守性が低下しました。formik: 単体で完結しており、外部依存が少ないため導入が簡単です。react-final-form: final-form というコアロジックを持つ別パッケージに依存していますが、軽量です。react-hook-form: React 本体以外の依存がなく、非常に軽量です。react-jsonschema-form: JSON Schema のパースやバリデーションに追加の依存を持つため、相対的にサイズが大きくなります。redux-form: Redux 本体に依存しており、すでに Redux を使っているプロジェクト以外ではオーバーヘッドが大きいです。| ライブラリ | 推奨シナリオ | 注意点 |
|---|---|---|
formik | 中規模フォームで素早く実装したい場合 | 大規模フォームでは再レンダリングに注意 |
react-final-form | 高度な制御と最適化が必要な場合 | 学習コストがやや高い |
react-hook-form | パフォーマンス重視・軽量さが求められる場合 | uncontrolled 設計に慣れる必要あり |
react-jsonschema-form | JSON Schema から動的フォーム生成が必要な場合 | カスタムロジックに制限あり |
redux-form | 新規プロジェクトでは使用禁止 | 非推奨、代替ライブラリへの移行を推奨 |
最終的には、プロジェクトの規模、チームのスキルセット、パフォーマンス要件、そして将来の保守性を総合的に判断することが重要です。特に新規プロジェクトでは、react-hook-form か formik のいずれかから始めるのが安全かつ実用的です。
react-hook-form はパフォーマンスと軽量さを最優先する場合に最適です。uncontrolled コンポーネントを活用して再レンダリングを最小限に抑え、TypeScript との親和性も高いです。ただし、完全に uncontrolled な設計のため、動的フィールド追加や高度な状態同期が必要なケースでは追加実装が必要になることがあります。
formik は中規模プロジェクトでバランスの取れた開発体験を求める場合に適しています。Context API を活用したシンプルな状態管理により、Redux などの外部ストアに依存せずとも複雑なフォームを構築できます。ただし、高頻度入力フィールド(例:リアルタイム検索)では不要な再レンダリングが発生しやすいため、パフォーマンス要件が厳しいケースには注意が必要です。
react-final-form は細かい制御と最適化を重視する開発者向けです。Field コンポーネント単位での再レンダリング抑制や、カスタムロジックの自由な実装が可能です。ただし、学習コストがやや高く、シンプルなフォームでは過剰な機能となることがあります。
react-jsonschema-form は JSON Schema に基づいてフォームを動的に生成する必要がある場合(例:設定UIジェネレータ、APIベースのフォーム定義)に選択すべきです。UIスキーマによるレイアウト制御も可能ですが、カスタムロジックや複雑なインタラクションには柔軟性に欠けることがあります。
redux-form は公式に非推奨(deprecated)とされており、新規プロジェクトでの使用は避けるべきです。既存の Redux ストアと深く統合されていたものの、状態がストア全体に分散しパフォーマンス上の問題を引き起こすことが多かったため、現在は react-final-form や react-hook-form への移行が推奨されています。
Get started | API | Form Builder | FAQs | Examples
npm install react-hook-form
import { useForm } from 'react-hook-form';
function App() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register('firstName')} />
<input {...register('lastName', { required: true })} />
{errors.lastName && <p>Last name is required.</p>}
<input {...register('age', { pattern: /\d+/ })} />
{errors.age && <p>Please enter number for age.</p>}
<input type="submit" />
</form>
);
}
We’re incredibly grateful to these kind and generous sponsors for their support!
Thank you to our previous sponsors for your generous support!
Thanks go to all our backers! [Become a backer].
Thanks go to these wonderful people! [Become a contributor].
Documentation website supported and backed by Vercel