Skip to content

v1.0.0-rc.0

Pre-release
Pre-release
Compare
Choose a tag to compare
@edmundhung edmundhung released this 22 Jan 22:52
· 153 commits to main since this release

This release candidate is a complete rewrite of the library.

You can find the update remix example at https://stackblitz.com/github/edmundhung/conform/tree/v1.0.0-rc.0/examples/remix
or try it out locally using the following command:

npm install @conform-to/react@next @conform-to/zod@next

Breaking Changes

  • The minimum react version supported is now React 18

  • All conform helpers are renamed.

    • conform.input -> getInputProps
    • conform.select -> getSelectProps
    • conform.textarea -> getTextareaProps
    • conform.fieldset -> getFieldsetProps
    • conform.collection -> getCollectionProps
  • The type option on getInputProps is now required.

function Example() {
  return <input {...getInputProps(fields.title, { type: 'text' })} />;
}
  • form.props is removed. You can use the helper getFormProps() instead.
import { getFormProps } from '@conform-to/react';

function Example() {
  const [form] = useForm();

  return <form {...getFormProps(form)} />;
}
  • conform.INTENT is removed. If you need to setup an intent button, please use the name "intent" or anything you preferred.

  • You will find conform.VALIDATION_UNDEFINED and conform.VALIDATION_SKIPPED on our zod integration (@conform-to/zod) instead.

    • conform.VALIDATION_UNDEFINED -> conformZodMessage.VALIDATION_UNDEFINED
    • conform.VALIDATION_SKIPPED -> conformZodMessage.VALIDATION_SKIPPED.
  • The parse helper on @conform-to/zod is now called parseWithZod with getFieldsetConstraint renamed to getZodConstraint

  • The parse helper on @conform-to/yup is now called parseWithYup with getFieldsetConstraint renamed to getYupConstraint

  • Both useFieldset and useFieldList hooks are removed. You can now use meta.getFieldset() or meta.getFieldList() instead.

function Example() {
  const [form, fields] = useForm();

  // Instead of `useFieldset(form.ref, fields.address)`, it is now:
  const address = fields.address.getFieldset();

  // Instead of `useFieldList(form.ref, fields.tasks)`, it is now:
  const tasks = fields.tasks.getFieldList();

  return (
    <form>
      <ul>
        {tasks.map((task) => {
          // It is no longer necessary to define an addtional component
          // As you can access the fieldset directly
          const taskFields = task.getFieldset();

          return <li key={task.key}>{/* ... */}</li>;
        })}
      </ul>
    </form>
  );
}

Improved submission handling

We have redesigned the submission object received after parsing the formdata to simplify the setup with a new reply API for you to set additional errors or reset the form.

export async function action({ request }: ActionArgs) {
  const formData = await request.formData();
  const submission = parse(formData, { schema });

  /**
   * The submission status could be either "success", "error" or undefined
   * If the status is undefined, it means that the submission is not ready (i.e. `intent` is not `submit`)
   */
  if (submission.status !== 'success') {
    return json(submission.reply(), {
      // You can also use the status to determine the HTTP status code
      status: submission.status === 'error' ? 400 : 200,
    });
  }

  const result = await save(submission.value);

  if (!result.successful) {
    return json(
      submission.reply({
        // You can also pass additional error to the `reply` method
        formError: ["Submission failed"],
        fieldError: {
          address: ["Address is invalid"],
        },

        // or avoid sending the the field value back to client by specifying the field names
        hideFields: ['password'],
      })
    );
  } 

  // Reply the submission with `resetForm` option
  return json(submission.reply({ resetForm: true }));
}

export default function Example() {
  const lastResult = useActionData<typeof action>();
  const [form, fields] = useForm({
    // `lastSubmission` is renamed to `lastResult` to avoid confusion
    lastResult,
  });

  // We can now find out the status of the submission from the form metadata as well 
  console.log(form.status); // "success", "error" or undefined
}

Simplified integration with the useInputControl hook

The useInputEvent hook is replaced by the useInputControl hook with some new features.

  • There is no need to provide a ref of the inner input element anymore. It looks up the input element from the DOM and will insert one for you if it is not found.

  • You can now use control.value to integrate a custom input as a controlled input and update the value state through control.change(value). The value will also be reset when a form reset happens

import { useInputControl } from '@conform-to/react';
import { CustomSelect } from './some-ui-library';

function Example() {
  const [form, fields] = useForm();
  const control = useInputControl(fields.title);

  return (
    <CustomSelect
      name={fields.title.name}
      value={control.value}
      onChange={(e) => control.change(e.target.value)}
      onFocus={control.focus}
      onBlur={control.blur}
    />
  );
}

Refined intent button setup

  • Both validate and list exports are removed in favor of setting up through the form metadata object.
    • validate -> form.validate
    • list.insert -> form.insert
    • list.remove -> form.remove
    • list.reorder -> form.reorder
    • list.replace -> form.update
function Example() {
  const [form, fields] = useForm();
  const tasks = fields.tasks.getFieldList();

  return (
    <form>
      <ul>
        {tasks.map((task) => {
          return <li key={task.key}>{/* ... */}</li>;
        })}
      </ul>
      <button {...form.insert.getButtonProps({ name: fields.tasks.name })}>
        Add (Declarative API)
      </button>
            <button onClick={() => form.insert({ name: fields.tasks.name })}>
        Add (Imperative API)
            </button>
    </form>
  );
}
  • You can now reset a form with form.reset or update any field value with form.update

Form Context

By setting up a react context with the <FormProvider />, we will now be able to subscribe to the form metadata using the useField() hook. This not only avoids prop drilling but also prevent unneccessary re-renders by tracking the usage of indivudal metadata through a proxy and only rerender it if the relevant metadata is changed.

The <FormProvider /> can also be nesteded with different form context and Conform will look up the closest form context unless a formId is provided.

import { type FieldName, FormProvider, useForm, useField } from '@conform-to/react';

function Example() {
  const [form, fields] = useForm({ ... });

  return (
    <FormProvider context={form.context}>
      <form>
        <AddressFieldset name={fields.address.name} />
      </form>
    </FormProvider>
  );
}

// The `FieldName<Schema>` type is basically a string with additional type information encoded
type AddressFieldsetProps = {
  name: FieldName<Address>
}

export function AddressFieldset({ name }: AddressFieldsetProps) {
  const [meta] = useField(name);
  const address = meta.getFieldset();

  // ...
}

If you want to create a custom input component, it is now possible too!

import { type FieldName, FormProvider, useForm, useField, getInputProps } from '@conform-to/react';

function Example() {
  const [form, fields] = useForm({ ... });

  return (
    <FormProvider context={form.context}>
      <form>
        <CustomInput name={fields.title.name} />
      </form>
    </FormProvider>
  );
}

type InputProps = {
  name: FieldName<string>
}

// Make your own custom input component!
function CustomInput({ name }: InputProps) {
  const [
    meta,
    form, // You can also access the form metadata directly
  ] = useField(name);

  return (
    <input {...getInputProps(meta)} />
  );
}

Similarly, you can access the form metadata on any component using the useFormMetadata() hook:

import { type FormId, FormProvider, useForm, getFormProps } from '@conform-to/react';

function Example() {
  const [form, fields] = useForm({ ... });

  return (
    <FormProvider context={form.context}>
      <CustomForm id={form.id}>
        {/* ... */}
      </CustomForm>
    </FormProvider>
  );
}

function CustomForm({ id, children }: { id: FormId; children: ReactNode }) {
  const form = useFormMetadata(id);

  return (
    <form {...getFormProps(form)}>
      {children}
    </form>
  );
}