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