はじめに
業務アプリでは、フォームのバリデーション設計がUI/UXの質や保守性に直結します。本記事では、私が実際に設計・実装した「汎用的なバリデーション設計例」を紹介します。
背景
- フォームの項目が多くなると、各項目にバリデーションロジックが散在しやすくなる。
- バリデーションロジックを再利用したく、フォーム開発を楽になりたい。
- 保守・拡張がしやすい共通的な仕組みが必要。
設計方針
項目 | 内容 |
---|---|
バリデーションのルール | 配列として各項目に持たせ、柔軟に追加できるように。 |
バリデーション実行 | 汎用関数 formItemsVali で一括管理。 |
バリデーション結果 | 各 Form.Item の validateStatus , help に即反映。 |
他項目依存バリデーション | バリデーション関数の引数に全体オブジェクトを渡す。 |
実装例
- バリデーション関数
validation.ts
// 空値チェック(文字列・数値対応) export const required = (value: string | number | null | undefined): boolean => { if (value === null || value === undefined) return false; if (typeof value === 'string') return value.trim() !== ''; return true; }; // 冊数の整合性(他項目依存) export const fewerOfCount = ( value: number, valueObj: {count: number, countOnBorrowed: number} ): boolean => { const { count } = valueObj; return count >= value; }
- バリデーション実行ロジック
validation.ts
export const formItemsVali = ( formItems: CommFormItemProps[], valueObj: { [key: string]: any } ) => { let valiRes = true; const newFormItems = formItems.map((item) => { if (item.validations && item.validations.length > 0) { const { itemProps } = item; const value = valueObj[itemProps.name]; for(const rule of item.validations) { const isValid = rule.vali(value, valueObj); if (!isValid) { item.itemProps.validateStatus = "error"; item.itemProps.help = rule.msg; valiRes = false; break; } } } return item; }); return { valiRes, formItems: newFormItems, } }
formItemsVali の役割
- 与えられたフォーム項目の配列(formItems)に対し、各項目のバリデーションルールを実行。
- バリデーション結果に応じて validateStatus や help を更新。
- フォーム全体が有効かどうか(valiRes)と更新後のフォーム項目を返す。
- UIの工夫
CommFormItem.tsx
const CommFormItem: React.FC<CommFormItemProps> = (props) => { const { itemProps, Comp, compProps } = props; return ( <Form.Item className="comm-form-item-container" {...itemProps} > <Comp {...compProps} /> </Form.Item> ) }
CommFormItem設計の考え
- 汎用性が高い
- Comp と compProps を分けることで、Input, Select, DatePicker, InputNumber など、どんなコンポーネントでも同じパターンで描画できます。
- 新しい部品(例: Upload, Switch)が必要になったときも、Comp にそのコンポーネントを指定するだけで済みます。
- バリデーションを一元管理
- バリデーション結果(validateStatus, help)も itemProps 経由で柔軟に制御可能。
- 保守性が高い
- もし将来<Form.Item>に追加の共通設定やデザイン変更が必要になっても、CommFormItem の内部だけを修正すれば全フォームに反映される。
- 使う例
BookEdit.tsx
// 抜粋
const BookEdit: React.FC<BookEditProps> = (props) => {
const [form] = Form.useForm();
// 動的に生成したフォーム項目定義を保持する state
const [commFormItems, setCommFormItems] = useState<CommFormItemProps[]>([]);
// 書籍情報(props.bookInfo)やカテゴリリスト(props.categoryOptions)の変更でフォーム項目を再生成
useEffect(() => {
form.setFieldsValue(bookInfo);
setCommFormItems([{
itemProps: {
label: "書籍タイトル",
name: "title",
className: "f-item comm-form-item-container"
},
validations: [{
vali: required,
msg: "入力が必要です。",
}],
Comp: Input,
compProps: {
value: bookInfo.title,
maxLength: 200,
showCount: true,
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
onItemChange("title", e.target.value);
}
},
}, {
itemProps: {
label: "カテゴリ",
name: "category",
className: "category-item f-item"
},
validations: [{
vali: required,
msg: "入力が必要です。",
}],
Comp: Select,
compProps: {
value: bookInfo.category,
options: categoryOptions,
onChange: (value: number) => onItemChange("category", value),
},
}, {
itemProps: {
label: "サマリー",
name: "summary",
className: "f-item comm-form-item-container"
},
validations: [],
Comp: Input.TextArea,
compProps: {
maxLength: 500,
showCount: true,
value: bookInfo.summary,
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
onItemChange("summary", e.target.value);
}
},
}, {
itemProps: {
label: "著者名",
name: "author",
className: "f-item comm-form-item-container"
},
validations: [],
Comp: Input,
compProps: {
maxLength: 100,
showCount: true,
value: bookInfo.author,
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
onItemChange("author", e.target.value);
}
},
}, {
itemProps: {
label: "ISBNコード",
name: "isbn",
className: "f-item comm-form-item-container"
},
validations: [],
Comp: Input,
compProps: {
maxLength: 20,
showCount: true,
value: bookInfo.isbn,
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
onItemChange("isbn", e.target.value);
}
},
}, {
itemProps: {
label: "出版社名",
name: "publisher",
className: "f-item comm-form-item-container"
},
validations: [],
Comp: Input,
compProps: {
maxLength: 200,
showCount: true,
value: bookInfo.publisher,
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
onItemChange("publisher", e.target.value);
}
},
}, {
itemProps: {
label: "出版日付",
name: "publishDate",
className: "count-item f-item"
},
validations: [],
Comp: DatePicker,
compProps: {
value: bookInfo.publishDate,
onChange: (value: string) => {
onItemChange("publishDate", value);
}
},
}, {
itemProps: {
label: "冊数",
name: "count",
className: "count-item"
},
validations: [{
vali: required,
msg: "入力が必要です。"
}],
Comp: InputNumber,
compProps: {
value: bookInfo.count,
onChange: (value: number) => {
onItemChange("count", value);
}
},
}, {
itemProps: {
label: "貸出中冊数",
name: "countOnBorrowed",
className: "count-item"
},
validations: [{
vali: required,
msg: "入力が必要です。"
}, {
vali: fewerOfCount,
msg: "貸出中の冊数が、冊数を超えてはいけません。",
}],
Comp: InputNumber,
compProps: {
value: bookInfo.countOnBorrowed,
onChange: (value: number) => {
onItemChange("countOnBorrowed", value);
}
},
}]);
}, [bookInfo]);
// 入力値の変更時に呼ばれる
const onItemChange = (key: string, value: string | number) => {
setBookInfo(prevBookInfo => ({
...prevBookInfo,
[key]: value,
}));
}
// 確定ボタンクリック時の処理
const onSubmit = () => {
const { valiRes, formItems } = formItemsVali(commFormItems, bookInfo);
if (!valiRes) {
setCommFormItems(formItems);
return;
}
// バリデーションOKの場合、サーバ送信などの後続処理
...
}
return (
<section className="comm-book-edit-container">
<Form name="loginForm" form={form}>
{
commFormItems.map((items, index) => (
<CommFormItem key={index} {...items} />
))
}
</Form>
<Button type="primary" onClick={onSubmit}>
確定
</Button>
</section>
)
}
まとめ
本記事では、業務アプリにおける 汎用的かつ保守性の高いバリデーション設計 の具体例を紹介しました。今回の設計のポイントは以下の通りです。
- バリデーションロジックの再利用性が高い
- バリデーション関数を汎用化し、どのフォーム項目にも簡単に適用可能。
- 他項目依存バリデーションも対応でき、実業務で求められる複雑なチェックにも強い。
- コードの見通しが良く、保守が楽
- 項目定義・バリデーション・UI描画がデータ駆動型で統一。
- 個別の if 文や分岐処理が散在せず、追加や変更がしやすい。
- UI/UX の質が向上
- バリデーション結果が即座に画面に反映され、ユーザーが入力ミスに気づきやすい。
- フォーム部品の描画も汎用化され、デザインや振る舞いの変更が簡単。
- 将来の拡張性が高い
- 新しいバリデーションルールやコンポーネントも既存の枠組みに乗せやすい。
- 共通の CommFormItem を介することで、大規模フォームでも一貫性を保てる。
最後に
フォームバリデーションは業務アプリのUI品質に直結し、かつ保守負荷の高い領域です。
今回の設計はその負荷を軽減し、チームでの開発や運用をスムーズにする一つのアプローチです。
皆さんのプロジェクトにもぜひ取り入れていただき、より良いフォーム開発のヒントになれば幸いです。