はじめに
数十項目におよぶ管理画面のフォームで、1文字入力するたびに画面全体が再レンダリングされて動作がもっさりする——そんな経験はありませんか?さらに「この項目を選んだらあの項目を表示する」「A欄の値に応じてB欄の選択肢を変える」といった項目間の連動が積み重なると、フォームのコードはあっという間に読み解けないスパゲッティになってしまいます。
この「エンタープライズ級フォームのつらさ」に真正面から取り組んだのが、Alibabaが開発するオープンソースライブラリFormilyです。フィールド単位の高精度な状態管理とJSON Schemaによる宣言的なフォーム定義で、巨大で複雑なフォームでも快適に開発・動作させることができます。
説明より先に、実際に動くフォームを触ってみるのが早いと思います。createFormとFieldだけで組んだ最小構成のサンプルを用意したので、先に挙動を確認したい方はこちらからどうぞ。
Formilyとは
Formilyは、Alibabaが自社の大規模な業務システム開発の知見をもとに作り上げたフォームソリューションです。GitHubのスター数は12,000を超え、React・React Native・Vue 2・Vue 3と複数のフレームワークに対応しています。単なる「フォームのバリデーションライブラリ」ではなく、状態管理・レイアウト・項目間連動・スキーマ駆動レンダリングまでカバーする、フォーム開発の総合フレームワークと呼ぶべき存在です。
主な特徴
- 高精度レンダリング - 各フィールドの状態を独立して管理する分散型アーキテクチャを採用しており、1つのフィールドへの入力がフォームツリー全体の再レンダリングを引き起こしません。巨大なフォームでもパフォーマンスが落ちにくい設計です
- JSON Schema駆動 - フォームの構造をJSON Schemaで宣言的に定義できます。バックエンドから受け取ったスキーマでフォームを動的に描画する、といった「サーバー駆動UI」も実現できます
- 強力な項目間連動(リアクション) - 「この値が変わったらあのフィールドを制御する」という連動ロジックを、
x-reactionsという宣言的な仕組みで記述できます - マルチフレームワーク対応 - コアロジックは
@formily/coreに集約されており、React用・Vue用のブリッジを差し替えるだけで同じ思想のままフレームワークを横断できます - UIライブラリ統合 - Ant Design(v4/v5)やAlibaba Fusion向けの統合コンポーネントが公式提供されており、フォームレイアウトまで含めてすぐに使い始められます
インストール
Reactプロジェクトでは、コアとReactブリッジの2つをインストールします。
# npm
npm install @formily/core @formily/react --save
# yarn
yarn add @formily/core @formily/react
# pnpm
pnpm add @formily/core @formily/react
Ant Design v5と組み合わせる場合は、統合パッケージも追加します。
npm install @formily/antd-v5 antd --save
Vue 3の場合は@formily/vueを使います。
npm install @formily/core @formily/vue --save
本記事執筆時点の最新バージョンは@formily/core v2.3.7です。
Formilyのサンプルを動かす
下のフォームはFormilyのcreateFormとFieldコンポーネントだけで作ったユーザー名入力欄です。createFormが返すフォームインスタンスのeffectsにonFieldValueChangeを登録し、usernameフィールドの値が変わるたびに文字数をリアルタイムに数えて表示しています。Formilyの各フィールドは独立したリアクティブユニットとして管理されるため、この入力に反応しているのは文字数表示だけで、フォーム全体が再レンダリングされているわけではありません。
要点だけを抜き出すと、次のような構成です。
const form = createForm({
effects() {
// usernameフィールドの値が変わるたびに呼ばれる
onFieldValueChange('username', (field) => {
setCount((field.value ?? '').length)
})
},
})
<FormProvider form={form}>
<Field name="username" component={[TextInput]} />
</FormProvider>
実際に動かせるものが下です。入力欄の文字を消したり打ち足したりすると、文字数の表示が即座に切り替わります。3文字未満にすると警告表示に変わるので、count >= 3の条件を書き換えれば必要な文字数のしきい値も自由に調整できます。
Fieldのcomponentプロパティには、valueとonChangeを受け取る任意のReactコンポーネントを渡せます。onFieldValueChangeは@formily/coreが提供するライフサイクルフックで、対象フィールド名を第一引数に、変化時のコールバックを第二引数に渡すだけで購読できる手軽さがポイントです。
基本的な使い方
まずは最小構成として、Fieldコンポーネントでログインフォームを組んでみます。ポイントは、フォームのインスタンスをcreateForm()で作り、FormProviderで配下に共有するという流れです。
import React from 'react';
import { createForm } from '@formily/core';
import { FormProvider, Field } from '@formily/react';
import { Form, FormItem, Input, Password, Submit } from '@formily/antd-v5';
// フォームインスタンスはコンポーネント外(またはuseMemo)で生成します
const form = createForm();
const Login = () => (
<FormProvider form={form}>
<Form layout="vertical">
<Field
name="username"
title="ユーザー名"
required
decorator={[FormItem]}
component={[Input, { placeholder: 'ユーザー名を入力' }]}
/>
<Field
name="password"
title="パスワード"
required
decorator={[FormItem]}
component={[Password, { placeholder: 'パスワードを入力' }]}
/>
<Submit block onSubmit={(values) => console.log(values)}>
ログイン
</Submit>
</Form>
</FormProvider>
);
export default Login;
decoratorにはフィールドを包むラッパー(ラベルやエラー表示を担うFormItem)を、componentには実際の入力コンポーネントを指定します。requiredを付けるだけで必須バリデーションが有効になり、エラーメッセージの表示まで自動で行われます。
ここで注目したいのは、usernameに入力してもフォーム全体は再レンダリングされないという点です。Formilyは各フィールドをリアクティブな独立ユニットとして管理しているため、変更の影響範囲が最小限に抑えられます。
JSON Schemaでフォームを宣言する
Formilyの真骨頂はスキーマ駆動です。createSchemaFieldで使用するコンポーネントを登録し、フォームの構造をJSONで記述します。
import React from 'react';
import { createForm } from '@formily/core';
import { FormProvider, createSchemaField } from '@formily/react';
import { Form, FormItem, Input, Select, NumberPicker, Submit } from '@formily/antd-v5';
const form = createForm();
const SchemaField = createSchemaField({
components: { FormItem, Input, Select, NumberPicker },
});
const schema = {
type: 'object',
properties: {
name: {
type: 'string',
title: '氏名',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
age: {
type: 'number',
title: '年齢',
'x-decorator': 'FormItem',
'x-component': 'NumberPicker',
'x-component-props': { min: 0, max: 120 },
},
plan: {
type: 'string',
title: 'プラン',
required: true,
enum: [
{ label: '無料プラン', value: 'free' },
{ label: '有料プラン', value: 'pro' },
],
'x-decorator': 'FormItem',
'x-component': 'Select',
},
},
};
const Register = () => (
<FormProvider form={form}>
<Form layout="vertical">
<SchemaField schema={schema} />
<Submit block onSubmit={(values) => console.log(values)}>
登録
</Submit>
</Form>
</FormProvider>
);
export default Register;
フォームの構造がただのJavaScriptオブジェクトになったことで、「スキーマをAPIから取得して動的にフォームを組み立てる」「スキーマをDBに保存してノーコード風のフォームビルダーを作る」といった応用が一気に現実的になります。
Formilyのサンプルを動かす:スキーマ駆動フォームと値のリアルタイム表示
createSchemaFieldで登録したコンポーネントは、スキーマのx-componentに文字列で指定するだけで描画に使われます。ここではonFormValuesChangeという@formily/coreのフォーム全体向けフックを使い、スキーマから生成されたフォームの現在値をJSONとしてリアルタイムに表示してみます。
const form = createForm({
effects() {
// フォーム内のどれかの値が変わるたびに呼ばれる
onFormValuesChange((f) => setValues({ ...f.values }))
},
})
const schema = {
type: 'object',
properties: {
name: { type: 'string', 'x-component': 'TextInput' },
plan: { type: 'string', default: 'free', 'x-component': 'SelectInput' },
},
}
実際に動かせるものが下です。氏名欄に入力したりプランを切り替えたりすると、下に表示されているJSONがその場で更新されます。
SchemaFieldに渡したschemaオブジェクトのpropertiesを増やせば、そのままJSON表示にも項目が増えます。バックエンドから取得したスキーマをそのままschemaに渡す構成にすれば、フォームの見た目をコード修正なしで変更できる「サーバー駆動UI」の感触がつかめるはずです。
実践的なユースケース:項目間の連動
実務のフォームで最も厄介なのが項目間の連動です。ここでは「有料プランを選んだときだけ支払い方法を表示する」という定番の要件を、x-reactionsで宣言的に実装してみます。
const schema = {
type: 'object',
properties: {
plan: {
type: 'string',
title: 'プラン',
required: true,
enum: [
{ label: '無料プラン', value: 'free' },
{ label: '有料プラン', value: 'pro' },
],
'x-decorator': 'FormItem',
'x-component': 'Select',
},
payment: {
type: 'string',
title: '支払い方法',
enum: [
{ label: 'クレジットカード', value: 'credit' },
{ label: '銀行振込', value: 'bank' },
],
'x-decorator': 'FormItem',
'x-component': 'Select',
// planの値を監視し、'pro'のときだけ表示・必須にする
'x-reactions': {
dependencies: ['plan'],
fulfill: {
state: {
visible: '{{$deps[0] === "pro"}}',
required: '{{$deps[0] === "pro"}}',
},
},
},
},
},
};
dependenciesで監視対象のフィールドを指定し、fulfill.stateで「条件を満たしたときの状態」を記述します。{{}}内は式として評価され、$deps[0]には依存フィールドの現在値が入ります。ifとuseEffectを重ねた命令的な制御コードと比べて、「何がどう連動するのか」が一目瞭然です。
コードで細かく制御したい場合は、createFormのeffectsにライフサイクルフックを書く方法もあります。
import { createForm, onFieldValueChange } from '@formily/core';
const form = createForm({
effects() {
// プラン変更時に支払い方法をリセットする
onFieldValueChange('plan', (field, form) => {
form.setFieldState('payment', (state) => {
state.value = undefined;
});
});
},
});
宣言的なx-reactionsとプログラマブルなeffectsを使い分けられるのが、Formilyの懐の深さです。
Formilyのサンプルを動かす:x-reactionsによる項目間連動
x-reactionsは、あるフィールドの値をトリガーに別フィールドの状態(表示・必須・値など)を宣言的に切り替える仕組みです。次のサンプルでは、planフィールドの値をdependenciesで監視し、fulfill.state.visibleの式がtrueになったときだけpaymentフィールドを表示しています。
payment: {
type: 'string',
'x-component': 'SelectInput',
// planフィールドを監視し、'pro'のときだけ表示する
'x-reactions': {
dependencies: ['plan'],
fulfill: {
state: {
visible: '{{$deps[0] === "pro"}}',
},
},
},
},
実際に動かせるものが下です。プランを「有料プラン」に切り替えると、支払い方法の選択欄がその場で現れます。「無料プラン」に戻すと再び隠れる点も確認してみてください。
dependencies: ['plan']を['plan', 'payment']のように増やせば複数フィールドを監視対象にでき、fulfill.stateにrequiredやvalueを追加すれば「表示するだけでなく必須にする」「初期値を書き換える」といった制御も同じ書き方で表現できます。命令的なif文を積み重ねる代わりに、連動ロジックがスキーマの中に集約されるのがFormilyらしいところです。
React Hook Formとの使い分け
「フォームライブラリならReact Hook Formで十分では?」と思った方もいるでしょう。実際、項目数が少ないシンプルなフォームならReact Hook Formのほうが軽量で手早く書けます。
Formilyが輝くのは、次のような条件が重なったときです。
- 項目数が数十を超え、ステップやタブをまたぐ大規模フォーム
- 項目間の連動・表示制御が多く、仕様変更も頻繁にある
- フォーム定義をサーバーサイドやDBで管理し、動的に描画したい
- ReactとVueが混在するプロダクト群でフォームのノウハウを共通化したい
つまり「フォームそのものがプロダクトの中核」であるような業務システム・管理画面・SaaSの設定画面では、Formilyのアーキテクチャが本領を発揮します。
まとめ
Formilyについて、基本から実践的な使い方までを紹介しました。
- FormilyはAlibaba発のフォームフレームワークで、React/Vueの両方に対応しています
- フィールド単位の独立した状態管理により、巨大フォームでも全体再レンダリングが発生しません
- JSON Schema駆動でフォームを宣言でき、サーバー駆動UIやフォームビルダーへの応用が可能です
x-reactionsとeffectsで、複雑な項目間連動を宣言的に記述できます
まずは手元の管理画面の中で「一番つらいフォーム」をひとつ選んで、Formilyで書き直してみてください。連動ロジックがスキーマに整理されていく気持ちよさは、一度体験すると戻れなくなるはずです。
