Future APIs / useIntent

useIntent

The useIntent function is part of Conform's future export. These APIs are experimental and may change in minor versions. Learn more

A React hook that returns an intent dispatcher for triggering form actions (validate, reset, update, insert, remove, reorder) without submitting the form. Use it for buttons or controls that need to modify form state.

import { useIntent } from '@conform-to/react/future';

const intent = useIntent(formRef);

#Parameters

formRef: FormRef

A reference to the form element. Can be either:

  • A React ref object pointing to a form element (e.g. React.RefObject<HTMLFormElement>, React.RefObject<HTMLButtonElement>, React.RefObject<HTMLInputElement>).
  • A form ID string to target a specific form

#Returns

An IntentDispatcher object with the following methods:

validate(name?: string): void

Triggers validation for the entire form or a specific field. If you provide a name that includes nested fields (e.g. user.email), it will validate all fields within that fieldset.

  • name (optional): Field name to validate. If omitted, validates the entire form.

reset(options?): void

Resets the form to a specific default value.

  • options.defaultValue (optional): The value to reset the form to. If not provided, resets to the default value from useForm. Pass null to clear all fields, or pass a custom object to reset to a specific state.

update(options): void

Updates a field or fieldset with new values.

  • options.name (optional): Field name to update. If omitted, updates the entire form.
  • options.index (optional): Array index when updating array fields.
  • options.value: New value for the field or fieldset.

insert(options): void

Inserts a new item into an array field.

  • options.name: Name of the array field.
  • options.index (optional): Position to insert at. If omitted, appends to the end.
  • options.defaultValue (optional): Default value for the new item.
  • options.from (optional): Name of a field to read the value from. When specified, the value is read from this field, validated, and if valid, inserted into the array while the source field is cleared. If validation fails, the insert is cancelled and the error is shown on the source field. Requires the error to be available synchronously.
  • options.onInvalid (optional): What to do when the insert causes a validation error on the array (e.g., exceeding max items). Currently only supports 'revert' to cancel the insert. Requires the error to be available synchronously.

remove(options): void

Removes an item from an array field.

  • options.name: Name of the array field.
  • options.index: Index of the item to remove.
  • options.onInvalid (optional): What to do when the remove causes a validation error on the array (e.g., going below min items). Requires the error to be available synchronously.
    • 'revert': Cancel the removal, keep the item as-is.
    • 'insert': Remove the item but insert a new blank item at the end. Use with defaultValue to specify the value.
  • options.defaultValue (optional): The default value for the new item when onInvalid is 'insert'.

reorder(options): void

Reorders items within an array field.

  • options.name: Name of the array field.
  • options.from: Current index of the item.
  • options.to: Target index to move the item to.

#Examples

Reusable reset button

1import { useRef } from 'react';
2import { useIntent, useForm } from '@conform-to/react/future';
3
4function ResetButton() {
5  const buttonRef = useRef<HTMLButtonElement>(null);
6  const intent = useIntent(buttonRef);
7
8  return (
9    <button type="button" ref={buttonRef} onClick={() => intent.reset()}>
10      Reset Form
11    </button>
12  );
13}
14
15function MyForm() {
16  const { form } = useForm({
17    // ...
18  });
19
20  return (
21    <form {...form.props}>
22      <input name="email" type="email" />
23      <input name="password" type="password" />
24      <button>Submit</button>
25      <ResetButton />
26    </form>
27  );
28}

Enter key to add array item

1import { useRef } from 'react';
2import { useIntent, useForm } from '@conform-to/react/future';
3
4function TagInput({ name }: { name: string }) {
5  const inputRef = useRef<HTMLInputElement>(null);
6  const intent = useIntent(inputRef);
7
8  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
9    if (e.key === 'Enter' && e.currentTarget.value.trim()) {
10      e.preventDefault();
11      intent.insert({
12        name,
13        defaultValue: e.currentTarget.value.trim(),
14      });
15      e.currentTarget.value = '';
16    }
17  };
18
19  return (
20    <input
21      ref={inputRef}
22      type="text"
23      placeholder="Type and press Enter to add..."
24      onKeyDown={handleKeyDown}
25    />
26  );
27}
28
29function MyForm() {
30  const { form, fields } = useForm({
31    defaultValue: {
32      tags: ['react', 'forms'],
33    },
34  });
35  const tagFields = fields.tags.getFieldList();
36
37  return (
38    <form {...form.props}>
39      <div>
40        <label>Tags</label>
41        <div>
42          {tagFields.map((tag, index) => (
43            <span key={tag.key}>
44              <input
45                type="hidden"
46                name={tag.name}
47                defaultValue={tag.defaultValue}
48              />
49              {tag.defaultValue}
50            </span>
51          ))}
52        </div>
53        <TagInput name="tags" />
54      </div>
55      <button>Submit</button>
56    </form>
57  );
58}

Inserting from another field

Use the from option to populate the new array item from another field. The value is validated before inserting. If valid, the value is inserted and the source field is cleared. If invalid, the insert is cancelled and the error is shown on the source field.

1import { useForm } from '@conform-to/react/future';
2import { coerceFormValue } from '@conform-to/zod';
3import { z } from 'zod';
4
5const schema = coerceFormValue(
6  z.object({
7    tags: z
8      .array(z.string({ required_error: 'Tag is required' }))
9      .max(5, 'Maximum 5 tags'),
10    newTag: z.string().optional(),
11  }),
12);
13
14function Example() {
15  const { form, fields, intent } = useForm(schema, {
16    // ...
17  });
18  const tagFields = fields.tags.getFieldList();
19
20  return (
21    <form {...form.props}>
22      <div>
23        {tagFields.map((tag, index) => (
24          <div key={tag.key}>
25            <input
26              type="hidden"
27              name={tag.name}
28              defaultValue={tag.defaultValue}
29            />
30            <span>{tag.defaultValue}</span>
31            <button
32              type="button"
33              onClick={() =>
34                intent.remove({
35                  name: fields.tags.name,
36                  index,
37                  onInvalid: 'revert',
38                })
39              }
40            >
41              Remove
42            </button>
43          </div>
44        ))}
45      </div>
46
47      <input name={fields.newTag.name} placeholder="New tag..." />
48      <div>{fields.newTag.errors}</div>
49
50      <button
51        type="button"
52        onClick={() =>
53          intent.insert({
54            name: fields.tags.name,
55            from: fields.newTag.name,
56          })
57        }
58      >
59        Add Tag
60      </button>
61
62      <button>Submit</button>
63    </form>
64  );
65}

#Tips

External buttons with form association

You can create intent buttons outside the form using the form attribute:

1import { useRef } from 'react';
2import { useIntent, useForm } from '@conform-to/react/future';
3
4function ResetButton({ form }: { form?: string }) {
5  const buttonRef = useRef<HTMLButtonElement>(null);
6  const intent = useIntent(buttonRef);
7
8  return (
9    <button
10      type="button"
11      ref={buttonRef}
12      form={form}
13      onClick={() => intent.reset()}
14    >
15      Reset Form
16    </button>
17  );
18}
19
20function MyApp() {
21  const { form, fields } = useForm({
22    // ...
23  });
24
25  return (
26    <>
27      <form {...form.props}>
28        <input name="title" />
29        <input name="description" />
30        <button type="submit">Submit</button>
31      </form>
32
33      {/* Button associated via form attribute */}
34      <ResetButton form={form.id} />
35    </>
36  );
37}

Dynamic insert and remove based on synchronous error

Array intents can change behavior based on validation errors when using onInvalid on insert/remove or from on insert. For example, reverting a remove intent that would go below the minimum item count. These decisions happen immediately, so the error must be available synchronously.

If you're using async validation, ensure array constraints are validated synchronously through a schema or by returning errors directly from onValidate:

1function Example() {
2  const { form, fields, intent } = useForm(schema, {
3    onValidate({ payload, error }) {
4      const emailErrors = error.fieldErrors.email ?? [];
5
6      if (emailErrors.length > 0) {
7        return error;
8      }
9
10      // Only perform async check if there are no sync email errors
11      const errorPromise = validateEmailUniqueness(payload.email).then(
12        (message) => {
13          if (!message) {
14            return error;
15          }
16
17          // Merge the synchronous errors with the async email error
18          return {
19            formErrors: error.formErrors,
20            fieldErrors: {
21              ...error.fieldErrors,
22              email: [message],
23            },
24          };
25        },
26      );
27
28      return [
29        // Synchronous validation result
30        error,
31        // Asynchronous validation result
32        errorPromise,
33      ];
34    },
35  });
36
37  // ...
38}