はじめに
数十項目におよぶ管理画面のフォームで、1文字入力するたびに画面全体が再レンダリングされて動作がもっさりする——そんな経験はありませんか?さらに「この項目を選んだらあの項目を表示する」「A欄の値に応じてB欄の選択肢を変える」といった項目間の連動が積み重なると、フォームのコードはあっという間に読み解けないスパゲッティになってしまいます。
この「エンタープライズ級フォームのつらさ」に真正面から取り組んだのが、Alibabaが開発するオープンソースライブラリFormilyです。フィールド単位の高精度な状態管理とJSON Schemaによる宣言的なフォーム定義で、巨大で複雑なフォームでも快適に開発・動作させることができます。
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です。
基本的な使い方
まずは最小構成として、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に保存してノーコード風のフォームビルダーを作る」といった応用が一気に現実的になります。
実践的なユースケース:項目間の連動
実務のフォームで最も厄介なのが項目間の連動です。ここでは「有料プランを選んだときだけ支払い方法を表示する」という定番の要件を、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の懐の深さです。
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で書き直してみてください。連動ロジックがスキーマに整理されていく気持ちよさは、一度体験すると戻れなくなるはずです。
