%% generate tags start %%
#software-engineering
%% generate tags end %%
#software-engineering/react
## Modular Form
### Quick Start
you can try out here: [Playground: Login form (React) | Modular Forms](https://modularforms.dev/react/playground/login)
```bash
pnpm install @modular-forms/react @preact/signals-react
```
```ts
import { … } from '@modular-forms/react';
```
define your schema and infer the type. We are using valibot here but you can use zod too.
```ts
import {
Input,
array,
boolean,
number,
object,
special,
string,
} from 'valibot';
const isFile = (input: unknown) => input instanceof File;
const SpecialSchema = object({
number: number(),
range: number(),
checkbox: object({
array: array(string()),
boolean: boolean(),
}),
select: object({
array: array(string()),
string: string(),
}),
file: object({
list: array(special<File>(isFile)),
item: special<File>(isFile),
}),
});
type SpecialForm = Input<typeof SpecialSchema>;
```
create your form component. The `Field` component does not render its own UI elements. It is headless and provides only the data layer of the field.
```tsx
import { useForm } from '@modular-forms/react';
type LoginForm = {
email: string;
password: string;
};
export default function App() {
const [loginForm, { Form, Field }] = useForm<LoginForm>();
return (
<Form>
<Field name="email">
{(field, props) => <input {...props} type="email" />}
</Field>
<Field name="password">
{(field, props) => <input {...props} type="password" />}
</Field>
<button type="submit">Login</button>
</Form>
);
}
```
field validation helpers if you are not using a validation schema.
```tsx
import { email, minLength, required, useForm } from '@modular-forms/react';
type LoginForm = {
email: string;
password: string;
};
export default function App() {
const [loginForm, { Form, Field }] = useForm<LoginForm>();
return (
<Form>
<Field
name="email"
validate={[
required('Please enter your email.'), // the rule and message
email('The email address is badly formatted.'),
]}
>
{(field, props) => (
<>
<input {...props} type="email" required />
{field.error && <div>{field.error}</div>}
</>
)}
</Field>
<Field
name="password"
validate={[
required('Please enter your password.'),
minLength(8, 'You password must have 8 characters or more.'),
]}
>
{(field, props) => (
<>
<input {...props} type="password" required />
{field.error && <div>{field.error}</div>}
</>
)}
</Field>
<button type="submit">Login</button>
</Form>
);
}
```
> [!info]- Time of validation
> By default, the first validation is done when the form is submitted for the first time and from there on, a revalidation is triggered after each user input. You can change this behavior using the `validateOn` and `revalidateOn` option of the [`useForm`](https://modularforms.dev/react/api/useForm) hook.
schema validation if you are using zod or valibot
```tsx
import { useForm, valiForm } from '@modular-forms/react';
import { Input, email, minLength, object, string } from 'valibot';
const LoginSchema = object({
email: string([
minLength(1, 'Please enter your email.'),
email('The email address is badly formatted.'),
]),
password: string([
minLength(1, 'Please enter your password.'),
minLength(8, 'You password must have 8 characters or more.'),
]),
});
type LoginForm = Input<typeof LoginSchema>;
export default function App() {
const [loginForm, { Form, Field }] = useForm<LoginForm>({
validate: valiForm(LoginSchema),
});
return (
<Form>
<Field name="email">
{(field, props) => (
<>
<input {...props} type="email" required />
{field.error && <div>{field.error}</div>}
</>
)}
</Field>
<Field name="password">
{(field, props) => (
<>
<input {...props} type="password" required />
{field.error && <div>{field.error}</div>}
</>
)}
</Field>
<button type="submit">Login</button>
</Form>
);
}
```
Example reusable component. Our goal is to develop a `TextInput` component so that the code ends up looking like this:
```tsx
<Field name="email" validate={…}>
{(field, props) => (
<TextInput
{...props}
type="email"
label="Email"
value={field.value}
error={field.error}
required
/>
)}
</Field>
```
Final code
```tsx
import { ReadonlySignal } from '@preact/signals-react';
import { ChangeEventHandler, FocusEventHandler, forwardRef } from 'react';
type TextInputProps = {
name: string;
type: 'text' | 'email' | 'tel' | 'password' | 'url' | 'date';
label?: string;
placeholder?: string;
value: ReadonlySignal<string | undefined>;
error: ReadonlySignal<string>;
required?: boolean;
onChange: ChangeEventHandler<HTMLInputElement>;
onBlur: FocusEventHandler<HTMLInputElement>;
};
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ label, value, error, ...props }, ref) => {
const { name, required } = props;
return (
<div>
{label && (
<label htmlFor={name}>
{label} {required && <span>*</span>}
</label>
)}
<input
{...props}
ref={ref}
id={name}
value={value.value || ''}
aria-invalid={!!error.value}
aria-errormessage={`${name}-error`}
/>
{error.value && <div id={`${name}-error`}>{error}</div>}
</div>
);
}
);
```
submit the values
```tsx
import { SubmitHandler, useForm } from '@modular-forms/react';
type LoginForm = {
email: string;
password: string;
};
export default function App() {
const [loginForm, { Form, Field }] = useForm<LoginForm>();
const handleSubmit: SubmitHandler<LoginForm> = (values, event) => {
// Your code here
};
return <Form onSubmit={handleSubmit}>…</Form>;
}
```
### Error Handling
you can have form error and field error after submission.
to throw form error, you just need to do this
```tsx
const handleSubmit: SubmitHandler<LoginForm> = (values, event) => {
if (error) {
throw new FormError<LoginForm>('An error has occurred.');
}
};
```
In addition to a general error message, you can also add an error message to specific fields using the second argument of the [`FormError`](https://modularforms.dev/react/api/FormError) class constructor.
```ts
if (error) {
throw new FormError<LoginForm>('An error has occurred.', {
email: 'This email has been blacklisted.',
});
}
```
To not display a general error message and instead display only errors of individual fields, you can simply omit the general message.
```ts
if (error) {
throw new FormError<LoginForm>({
email: 'This email has been blacklisted.',
});
}
```
> [!info] further
> - [Guide: Controlled fields (React) | Modular Forms](https://modularforms.dev/react/guides/controlled-fields)
> - [Guide: Transform inputs (React) | Modular Forms](https://modularforms.dev/react/guides/transform-inputs)
> - [Guide: Special inputs (React) | Modular Forms](https://modularforms.dev/react/guides/special-inputs)
> - [Guide: Nested fields (React) | Modular Forms](https://modularforms.dev/react/guides/nested-fields)
> - [Guide: Field arrays (React) | Modular Forms](https://modularforms.dev/react/guides/field-arrays)
## Conform
> [!info] see more
> [edmundhung/conform: Progressive enhancement first form validation library for Remix and React Router (github.com)](https://github.com/edmundhung/conform)
```ts
import { useForm } from '@conform-to/react';
import { parse } from '@conform-to/zod';
import { Form } from '@remix-run/react';
import { json } from '@remix-run/node';
import { z } from 'zod';
import { authenticate } from '~/auth';
const schema = z.object({
email: z
.string({ required_error: 'Email is required' })
.email('Email is invalid'),
password: z.string({ required_error: 'Password is required' }),
});
export async function action({ request }: ActionArgs) {
const formData = await request.formData();
const submission = parse(formData, { schema });
if (!submission.value || submission.intent !== 'submit') {
return json(submission);
}
return await authenticate(submission.value);
}
export default function LoginForm() {
const lastSubmission = useActionData<typeof action>();
const [form, { email, password }] = useForm({
lastSubmission,
onValidate({ formData }) {
return parse(formData, { schema });
},
});
return (
<Form method="post" {...form.props}>
<div>
<label>Email</label>
<input type="email" name={email.name} />
<div>{email.error}</div>
</div>
<div>
<label>Password</label>
<input type="password" name={password.name} />
<div>{password.error}</div>
</div>
<button>Login</button>
</Form>
);
}
```