これ、毎回書いてませんか?
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
async function handleSubmit() {
setLoading(true);
try {
await api.submit(data);
} catch (e) {
setError(e as Error);
} finally {
setLoading(false);
}
}
アプリの中で何十箇所も同じコードを書いている——そのモヤモヤを解消したくてOSSを作りました。
なぜ作ったか
Reddit や Stack Overflow のエンジニアリングコミュニティを調べていたとき、こんなスレッドをよく見かけました。
- What are your struggles when working with forms in react? — 「フォームライブラリ、設定多すぎ問題」
- Things I hate in web development in 2024 — 「ローディング状態を毎回手書きするの本当に嫌」
出てくる不満が驚くほど似ていて、しかもどれも「技術的に難しいわけじゃないけど、毎日何度も書くことになる」という種類のものでした。
既存のUIライブラリは「見た目(スタイル)」の提供で止まっているものがほとんどで、状態管理まで面倒を見てくれるものがないという穴があると気づいたのが出発点です。
作ったもの:behave-ui
「振る舞い込みコンポーネント」を提供するReact向けOSSです。
npx @behave-ui/cli@latest add async-button
コンポーネントは3つあります。
AsyncButton — 非同期状態を内包するボタン
冒頭のコードがこうなります。
<AsyncButton
onClick={() => api.submit(data)}
loadingText="送信中..."
successText="完了!"
errorText="失敗しました"
onSuccess={() => router.push('/done')}
onError={(err) => toast.error(err.message)}
>
送信する
</AsyncButton>
内部では以下のステートマシンが動いています。二重送信防止も自動です。
idle ──(click)──► pending ──(resolve)──► success ──(2秒後)──► idle
└───(reject)───► error ────(click)──► idle
AutoForm — Zodスキーマからフォームを自動生成
そもそもZodとは
ZodはTypeScriptのバリデーションライブラリです。「このオブジェクトはこういう型でなければならない」というルールをコードで書けます。
const schema = z.object({
name: z.string().min(1, '名前は必須です'),
email: z.string().email('形式が正しくありません'),
role: z.enum(['admin', 'user']),
});
これだけで「バリデーションルール」と「TypeScriptの型」が同時に手に入ります。
既存のAutoFormライブラリの問題
Zodスキーマからフォームを生成するライブラリは以前からありましたが、2つの問題がありました。
① Zod v4への追従が遅れている
2025年にZodのv4がリリースされ、内部構造が大きく変わりました。多くの既存ライブラリが正常に動かなくなっています。
// Zod v4で書いたスキーマを既存ライブラリに渡すと...
const schema = z.object({
age: z.number(),
role: z.enum(['admin', 'user']),
});
<ExistingAutoForm schema={schema} />
// → number フィールドの値が文字列になる
// → enum の選択肢が表示されない ← よくある不具合
behave-uiではv3とv4の両方に対応しています。
② 条件付きフィールドに非対応
「個人か法人かで入力項目が変わる」UIは実務でよく出てきますが、ほとんどのライブラリが対応していませんでした。これはRedditでもよく議論される話題です。
behave-uiでは z.discriminatedUnion() に対応しています。
const schema = z.discriminatedUnion('accountType', [
z.object({ accountType: z.literal('personal'), age: z.number() }),
z.object({ accountType: z.literal('company'), taxId: z.string() }),
]);
// これだけで、選択値に応じてフィールドが動的に切り替わる
<AutoForm schema={schema} onSubmit={handleSubmit} />
基本的な使い方
<AutoForm
schema={schema}
onSubmit={async (values) => await api.createUser(values)}
fieldConfig={{
bio: { label: '自己紹介', type: 'textarea' },
}}
/>
フィールドはスキーマから自動で推論されます。
| Zodの型 | 生成されるUI |
|---|---|
z.string() |
テキスト入力 |
z.string().email() |
メールアドレス入力 |
z.number() |
数値入力 |
z.boolean() |
チェックボックス |
z.enum([...]) |
セレクトボックス |
DataFetch — データ取得の4状態を1コンポーネントで
React State Management in 2025 でも指摘されているように、「リモート状態とクライアント状態の混在」がバグの温床になりやすいです。DataFetchはデータ取得に関わる状態を1か所に閉じ込めます。
// Before
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => { api.getUser(id).then(setData).catch(...) }, [id]);
if (loading) return <Skeleton />;
if (error) return <ErrorCard />;
if (!data) return <EmptyState />;
return <UserCard user={data} />;
// After
<DataFetch
queryKey={['user', userId]}
queryFn={() => api.getUser(userId)}
loadingFallback={<Skeleton />}
errorFallback={({ retry }) => <ErrorCard onRetry={retry} />}
emptyFallback={<EmptyState />}
>
{(user) => <UserCard user={user} />}
</DataFetch>
queryKey が変わると自動で再フェッチ、エラー時は自動リトライも内蔵しています。
メリット・デメリット
メリット
- ボイラープレートが消える — ローディング・エラー・成功の状態を毎回考えなくて済む
- 型安全 — Zodスキーマから型が自動生成されるので、タイポをコンパイル時に検出できる
- 段階的導入が可能 — 1コンポーネントだけ試せる。全体を書き直す必要はない
-
デバッグしやすい —
data-status="pending"などの属性で状態を外から観察できる
デメリット
- スタイルは自分で当てる — 振る舞いに特化しているのでCSSは提供していない
- 複雑なレイアウトのフォームは苦手 — 2カラムやフィールドグループが必要な場合はReact Hook Formを直接使う方がよいケースもある
- DataFetchはTanStack Queryほど高機能ではない — 楽観的更新・無限スクロールが必要ならTanStack Queryを使うべき
- まだ若いライブラリ — コミュニティが小さく、トラブル時に情報を探しにくい場合がある
こんな人におすすめ
-
管理画面・社内ツール・ダッシュボードを作っている人
デザインより機能を先に動かしたい場面で特に効果を発揮します -
スタートアップ・個人開発者
ボイラープレートを書いている時間を、ビジネスロジックに使いたい人 -
Zodをすでに使っているプロジェクト
既存のスキーマ定義をそのままフォームUIに変換できます -
「とりあえず動くもの」を速く作りたいチーム
shadcn/uiと同じコピペ方式なので、1コンポーネントから試せます
インストール
# コンポーネントをプロジェクトにコピー(shadcn/ui方式)
npx @behave-ui/cli@latest add async-button
npx @behave-ui/cli@latest add auto-form
npx @behave-ui/cli@latest add data-fetch
# npmパッケージとして使う場合
npm install @behave-ui/react
GitHubはこちらです。フィードバック・Issue・PRお待ちしています!