use-form-definition
v2.0.0
Published
A UI-agnostic React form library that generates forms from simple field definitions with Zod validation and React Hook Form integration
Maintainers
Readme
use-form-definition
A UI-agnostic React form library. You write one field definition; it generates the Zod schema, manages React Hook Form state, renders the form, and exposes the same definition for server-side validation.
When this saves you time
If you already reach for React Hook Form + Zod on most forms and end up repeating the same field metadata across the schema, the RHF setup, the JSX, and a server-side validator, this library bundles those four into one place.
If you only need one or two of those — for example, a single small form where writing a Zod schema by hand isn't a chore — the abstraction may not pay for itself. RHF or Zod on their own is usually enough in that case.
What it does
- One field definition drives the schema, form state, rendering, and server-side validation
- Works with any UI library (shadcn, MUI, Ant Design, your own components)
- Types are inferred from the definition; no manual sync between schema and form
- Same definition validates client-side and server-side (Next.js server actions, API routes)
- Built-in support for nested/repeater fields,
requiredWhenconditional rules, translation, and an async-validation plugin point - Reference components can be copied into your project via a CLI if you'd rather own them than import them
Installation
npm install use-form-definition react react-hook-form zodRequirements
- React >= 18.0.0
- React Hook Form >= 7.0.0
- Zod >= 3.0.0 < 4.0.0
Quick Start
1. Configure your form hook
// lib/form.ts
import { createFormDefinitionHook } from 'use-form-definition';
import { Input, Select, Field } from '@/components/form';
export const useFormDefinition = createFormDefinitionHook({
components: {
text: Input,
email: Input,
select: Select,
},
formComponents: {
Field: Field,
},
});2. Define your form
// forms/user.ts
import { FormDefinition } from 'use-form-definition';
export const userFormDefinition: FormDefinition = {
name: {
type: 'text',
label: 'Name',
validation: { required: true, minLength: 2 },
},
email: {
type: 'email',
label: 'Email',
validation: { required: true },
},
role: {
type: 'select',
label: 'Role',
options: [
{ value: 'admin', label: 'Admin' },
{ value: 'user', label: 'User' },
],
},
};3. Render your form
// components/UserForm.tsx
import { useFormDefinition } from '@/lib/form';
import { userFormDefinition } from '@/forms/user';
export function UserForm() {
const { RenderedForm } = useFormDefinition(userFormDefinition);
return <RenderedForm onSubmit={(data) => console.log(data)} />;
}Conditional Visibility
When a field's visibility depends on the form's current state (e.g. show "Please specify" only when "Other" is selected), drop down from <RenderedForm /> to manual rendering with <Form> and <RenderedField>, and use form.watch():
const { form, Form, RenderedField, Actions } = useFormDefinition(contactDefinition);
const subject = form.watch('subject');
return (
<Form onSubmit={form.handleSubmit(onSubmit)}>
<RenderedField name="name" />
<RenderedField name="email" />
<RenderedField name="subject" />
{subject === 'other' && <RenderedField name="customSubject" />}
<RenderedField name="message" />
<Actions />
</Form>
);The same pattern applies to per-field runtime props like disabled or options — pass them as props on <RenderedField> and the prop wins over the definition default. See examples/basic-react/src/pages/ContactPage.tsx for a working example.
Copy-and-Customize Components
Use the CLI to copy reference components to your project:
# Copy all basic components
npx use-form-definition copy all ./src/components/form/
# Copy individual components
npx use-form-definition copy text-input ./src/components/
npx use-form-definition copy field ./src/components/Documentation
- API Reference - Complete API documentation
- Plugin System - Extending validation and field types
- Repeater Fields - Dynamic list fields
- Type Inference - Automatic TypeScript types
Examples
See the examples directory for complete implementations:
| Example | Description | |---------|-------------| | basic-react | Core features with built-in unstyled components | | nextjs | Server actions, i18n, API routes | | mui | Material UI integration | | antd | Ant Design integration | | shadcn | shadcn/ui + Tailwind CSS |
Server Actions (Next.js)
Pass a server action to useFormDefinition and the form works with or without JavaScript: with JS it intercepts on submit, runs client-side validation, and dispatches the action; without JS the <form> posts natively to the server action, and the server's field errors render server-side. Render the result view from the returned actionState.
// app/users/actions.ts
'use server';
import { generateDataValidator, parseValidationErrors } from 'use-form-definition/server';
import { userFormDefinition } from './definition';
export async function createUser(prevState: unknown, formData: FormData) {
const result = generateDataValidator(userFormDefinition)(formData);
if (!result.success) {
return {
success: false as const,
errors: parseValidationErrors(result.error.issues),
values: Object.fromEntries(formData.entries()), // so fields repopulate without JS
};
}
// ...persist result.data...
return { success: true as const, data: result.data };
}// app/users/new-user-form.tsx
'use client';
import { useFormDefinition } from '@/lib/form';
import { userFormDefinition } from './definition';
import { createUser } from './actions';
export function NewUserForm() {
const { RenderedForm, actionState, isPending } = useFormDefinition(userFormDefinition, {
serverAction: createUser,
});
if (actionState?.success) return <p>Created {String(actionState.data?.name)}.</p>;
return <RenderedForm />; // server errors are shown on the fields automatically
}(isPending reflects the in-flight submission. Passing serverAction as a <RenderedForm serverAction={...}> prop also works, but only the hook option exposes actionState.)
Validation
Built-in validation rules:
validation: {
required: true,
minLength: 2,
maxLength: 100,
pattern: 'email', // Built-in patterns: email, url, phone, slug, username, etc.
matchValue: 'password', // Match another field
requiredWhen: { field: 'type', value: 'other' }, // Conditional
mustBeTrue: true, // For checkboxes
min: 0,
max: 100,
}Contributing
See CONTRIBUTING.md for development setup and guidelines.
License
MIT
