%% 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> ); } ```