Conform Documentation

repository·main·Indexed 25 days ago

https://github.com/edmundhung/conform

A React-based form library focused on progressive enhancement and type safety. Conform integrates with web standards and frameworks like Remix, Next.js (Server Actions), and Astro Actions. It provides the @conform-to/react package for form state management and offers schema validation adapters for Zod, Yup, and Valibot. The library includes guides for complex patterns like nested objects, arrays, and file uploads, as well as integration patterns for UI libraries such as Chakra UI, Headless UI, and Material UI.

Tokens
110K
Snippets
248
Records
446
Agent score
78%

What's inside Conform

  1. Overview of the React SPA Example

    main

    The React SPA example demonstrates how to use Conform in a Single Page Application (SPA) environment. It is built using React 19, React Router 8 (in declarative mode), Vite 8, and Zod 4.

    Key patterns demonstrated include:

    • Basic forms with manual validation: Implementing standard form logic.
    • Async validation: Handling validation that requires asynchronous checks (e.g., checking if a username exists).
    • Dynamic forms with data persistence: Managing forms where fields can change and data needs to be preserved.
  2. Introduction to Conform

    main

    Conform is a library designed to progressively enhance HTML forms using React. It enables the creation of resilient, type-safe forms by leveraging web standards.

    Key features include:

    • Full type safety: Achieved through schema field inference.
    • Standard Schema support: Enhanced integration with validation libraries like Zod and Valibot.
    • Progressive enhancement: Designed to work even when JavaScript is disabled, with built-in accessibility features.
    • Framework support: Native support for Server Actions in Remix and Next.js.
    • Web standards: Built on standard web APIs to allow flexible composition with other tools.
  3. Manually apply field metadata without getInputProps

    main

    The getInputProps helper is optional. If you prefer not to use it or need more granular control, you can manually spread field metadata onto your input elements. This is useful for reducing the abstraction layer or when working with complex custom logic.

    When doing this manually, you must ensure you include:

    • key, id, name, and form
    • defaultValue (for text/number) or defaultChecked (for checkboxes)
    • aria-invalid and aria-describedby based on the field's validity and error state.
    // Manual implementation example
    <input
      key={fields.task.key}
      id={fields.task.id}
      name={fields.task.name}
      form={fields.task.formId}
      defaultValue={fields.task.initialValue}
      aria-invalid={!fields.task.valid || undefined}
      aria-describedby={!fields.task.valid ? fields.task.errorId : undefined}
      required={fields.task.required}
      // ... other attributes
    />
  4. Accessibility best practices with useField

    main

    When building accessible forms with useField, use the provided auto-generated IDs to create proper ARIA associations:

    1. Input and Label: Use field.id for the <input id={...}> and htmlFor={field.id} on the <label>.
    2. Help Text: Use field.descriptionId for the aria-describedby attribute on the input to associate it with help text.
    3. Error Messages: Use field.errorId for the error message container's id and field.ariaDescribedBy on the input to associate the error with the field.
  5. Manually implement fieldset props without getFieldsetProps

    main

    The getFieldsetProps helper is optional. If you prefer not to use it or need to implement logic manually, you can use the field metadata directly to set the props of your fieldset element. This is useful for reducing dependency on the helper or when implementing custom logic for aria-describedby.

    // Manual implementation example
    function Example() {
      return (
        <fieldset
          id={fields.address.id}
          name={fields.address.name}
          form={fields.address.formId}
          aria-describedby={!form.valid ? form.errorId : undefined}
        />
      );
    }
  6. Control validation behavior with conformZodMessage

    main

    The conformZodMessage object provides special custom messages that allow you to control how Conform handles Zod validation results, which is particularly useful for managing asynchronous validation or conditional validation logic.

    By using these specific messages within a Zod superRefine or refine block, you can signal to Conform how to proceed when certain conditions are met:

    • conformZodMessage.VALIDATION_SKIPPED: Tells Conform that validation for this field was skipped. Conform will then use the previous validation result instead of treating this as an error.
    • conformZodMessage.VALIDATION_UNDEFINED: Tells Conform that validation is not defined for the current context. Conform will fallback to server-side validation.
    import { conformZodMessage } from '@conform-to/zod';
    
    // Example usage in a Zod refinement
    ctx.addIssue({
      code: 'custom',
      message: conformZodMessage.VALIDATION_SKIPPED,
    });
  7. Choose the right field access pattern

    main

    Depending on your component structure, use one of these three patterns to access field metadata:

    1. Static fields: Access directly via fields.fieldName when the field structure is known at compile time.
    2. Dynamic fields: Use form.getField(name), form.getFieldset(name), or form.getFieldList(name) to access fields by string name (useful for arrays or dynamic inputs).
    3. Field components: In child components, use the useField hook or useFormMetadata to access the necessary context.
  8. How Conform handles nested objects and arrays

    main

    Conform manages complex data structures by using a specific naming convention for the name attribute on input elements. This allows form data to be automatically parsed into nested JavaScript objects and arrays.

    Naming Convention

    • Nested Objects: Use object.property syntax (e.g., address.street).
    • Arrays: Use array[index] syntax (e.g., tasks[0]).
    • Nested Arrays: Combine both syntaxes (e.g., tasks[0].content).

    For example, if the form data contains ['tasks[0].content', 'Hello World'], Conform will construct the object { tasks: [{ content: 'Hello World' }] }.

  9. Implement async validation with server fallback

    main

    Conform handles async validation (like checking if an email is unique) by falling back to server validation when client-side validation is insufficient.

    1. On the Client: Use a schema creator that returns a schema without the async implementation. In the onValidate callback, if the async function is missing, use ctx.addIssue with message: conformZodMessage.VALIDATION_UNDEFINED and fatal: true. This signals Conform to trigger a server-side validation.
    2. On the Server: Use parseWithZod with the async: true option and provide the actual implementation of the async logic within the schema.
    import { refine } from '@conform-to/zod';
    
    function createSchema(
      options?: { isEmailUnique: (email: string) => Promise<boolean>; },
    ) {
      return z.object({
        email: z.string().email().pipe(
          z.string().superRefine((email, ctx) => {
            if (typeof options?.isEmailUnique !== 'function') {
              ctx.addIssue({
                code: 'custom',
                message: conformZodMessage.VALIDATION_UNDEFINED,
                fatal: true,
              });
              return;
            }
    
            return options.isEmailUnique(email).then((isUnique) => {
              if (!isUnique) {
                ctx.addIssue({
                  code: 'custom',
                  message: 'Email is already used',
                });
              }
            });
          }),
        ),
      });
    }
    
    export function action() {
      const formData = await request.formData();
      const submission = await parseWithZod(formData, {
        schema: createSchema({
          async isEmailUnique(email) { /* ... */ },
        }),
        async: true,
      });
    }
    
    export default function Signup() {
      const lastResult = useActionData();
      const [form] = useForm({
        lastResult,
        onValidate({ formData }) {
          return parseWithZod(formData, {
            schema: createSchema(),
          });
        },
      });
    }
  10. Disable automatic type coercion in parseWithZod

    main

    By default, parseWithZod automatically strips empty values and coerces form values to the types defined in your schema using an internal coerceFormValue helper.

    If you need to implement custom parsing logic (for example, handling specific string formats before converting them to numbers), set disableAutoCoercion: true in the options and use Zod's z.preprocess or similar methods to manage the values yourself.

    import { parseWithZod } from '@conform-to/zod';
    import { useForm } from '@conform-to/react';
    import { z } from 'zod';
    
    const schema = z.object({
      // Strip empty value and coerce the number yourself
      amount: z.preprocess((value) => {
        if (typeof value !== 'string') {
          return value;
        }
    
        if (value === '') {
          return undefined;
        }
    
        return Number(value.trim().replace(/,/g, ''));
      }, z.number()),
    });
    
    function Example() {
      const [form, fields] = useForm({
        onValidate({ formData }) {
          return parseWithZod(formData, {
            schema,
            disableAutoCoercion: true,
          });
        },
      });
    
      // ...
    }
  11. Configure schema validation with useForm

    main

    When you pass a Standard Schema as the first argument to useForm(schema, options), Conform enables schema-derived type inference and automatic validation.

    Key behaviors when using a schema:

    • Conform runs schema validation before the onValidate handler.
    • onValidate can still be used to add or replace validation errors.
    • ctx.schemaValue provides the parsed schema value when validation succeeds.
    • schemaOptions are forwarded to the configured schema validator.

    To support non-Standard Schema types, use configureForms.

    import { useForm } from '@conform-to/react/future';
    import { z } from 'zod';
    
    const schema = z.object({
      email: z.string().email('Email is invalid'),
      password: z.string().min(8, 'Password must be at least 8 characters'),
    });
    
    function LoginForm() {
      const { form, fields } = useForm(schema, {
        shouldValidate: 'onBlur',
      });
    
      return (
        <form {...form.props}>
          <input
            type="email"
            name={fields.email.name}
            defaultValue={fields.email.defaultValue}
          />
          <div>{fields.email.errors}</div>
          <input
            type="password"
            name={fields.password.name}
            defaultValue={fields.password.defaultValue}
          />
          <div>{fields.password.errors}</div>
    
          <button type="submit" disabled={!form.valid}>
            Login
          </button>
        </form>
      );
    }
  12. Manually implement form props without getFormProps

    main

    The getFormProps helper is optional. If you prefer not to use it or need to implement props manually, you can use the form metadata directly to set the properties of your form element. This is useful if you want full control over the attribute application.

    // Manual implementation example
    function Example() {
      return (
        <form
          id={form.id}
          onSubmit={form.onSubmit}
          noValidate={form.noValidate}
          aria-describedby={!form.valid ? form.errorId : undefined}
        />
      );
    }