Nested object and Array

Conform support both nested object and array by leveraging a naming convention on the name attribute.

Naming Convention

Conform uses the object.property and array[index] syntax to denote data structure. These notations could be combined for nested array as well. e.g. tasks[0].content.

The form data should be parsed using the Conform parse helper to resolve each data path and reconstruct the data structure accordingly.

import { parse } from '@conform-to/zod';

// If the form data has an entry `['tasks[0].content', 'Hello World']`
const submission = parse(formData, {
  /* ... */
});

// The submission payload will become `{ tasks: [{ content: 'Hello World' }] }`
console.log(submission.payload);

Nested Object

When you need to set up nested fields, you can pass the parent field config to the useFieldset hook to get access to each child field with name infered automatically.

1import { useForm, useFieldset } from '@conform-to/react';
2import { parse } from '@conform-to/zod';
3import { z } from 'zod';
4
5const schema = z.object({
6  address: z.object({
7    street: z.string(),
8    zipcode: z.string(),
9    city: z.string(),
10    country: z.string(),
11  }),
12});
13
14function Example() {
15  const [form, { address }] = useForm({
16    onValidate({ formData }) {
17      return parse(formData, { schema });
18    },
19  });
20  const { city, zipcode, street, country } = useFieldset(form.ref, address);
21
22  return (
23    <form {...form.props}>
24      {/* Set the name to `address.street`, `address.zipcode` etc. */}
25      <input name={street.name} />
26      <div>{street.error}</div>
27      <input name={zipcode.name} />
28      <div>{zipcode.error}</div>
29      <input name={city.name} />
30      <div>{city.error}</div>
31      <input name={country.name} />
32      <div>{country.error}</div>
33    </form>
34  );
35}
36

Array

When you need to setup a list of fields, you can pass the parent field config to the useFieldList hook to get access to each item field with name infered automatically as well.

1import { useForm, useFieldList } from '@conform-to/react';
2import { parse } from '@conform-to/zod';
3import { z } from 'zod';
4
5const schema = z.object({
6  tasks: z.array(z.string()),
7});
8
9function Example() {
10  const [form, { tasks }] = useForm({
11    onValidate({ formData }) {
12      return parse(formData, { schema });
13    },
14  });
15  const list = useFieldList(form.ref, tasks);
16
17  return (
18    <form {...form.props}>
19      <ul>
20        {list.map((task) => (
21          <li key={task.key}>
22            {/* Set the name to `task[0]`, `tasks[1]` etc */}
23            <input name={task.name} />
24            <div>{task.error}</div>
25          </li>
26        ))}
27      </ul>
28    </form>
29  );
30}
31

For information about modifying list (e.g. insert / remove / reorder), see the list intent section.

Nested Array

You can also combine both useFieldset and useFieldList hook for nested array.

1import type { FieldConfig } from '@conform-to/react';
2import { useForm, useFieldset, useFieldList } from '@conform-to/react';
3import { parse } from '@conform-to/zod';
4import { useRef } from 'react';
5import { z } from 'zod';
6
7const todoSchema = z.object({
8  title: z.string(),
9  notes: z.string(),
10});
11
12const schema = z.object({
13  tasks: z.array(todoSchema),
14});
15
16function Example() {
17  const [form, { tasks }] = useForm({
18    onValidate({ formData }) {
19      return parse(formData, { schema });
20    },
21  });
22  const todos = useFieldList(form.ref, tasks);
23
24  return (
25    <form {...form.props}>
26      <ul>
27        {todos.map((todo) => (
28          <li key={todo.key}>
29            {/* Pass each item config to TodoFieldset */}
30            <TodoFieldset config={todo} />
31          </li>
32        ))}
33      </ul>
34    </form>
35  );
36}
37
38function TodoFieldset({
39  config,
40}: {
41  config: FieldConfig<z.infer<typeof todoSchema>>;
42}) {
43  const ref = useRef<HTMLFieldSetElement>(null);
44  // Both useFieldset / useFieldList accept form or fieldset ref
45  const { title, notes } = useFieldset(ref, config);
46
47  return (
48    <fieldset ref={ref}>
49      <input name={title.name} />
50      <div>{title.error}</div>
51      <input name={notes.name} />
52      <div>{notes.error}</div>
53    </fieldset>
54  );
55}
56