Form Composition
Break forms into reusable components with useFormInstance.
Large forms benefit from being split into smaller, focused components. antd-typed provides useFormInstance to access form context from nested components, with two approaches depending on your needs.
The Problem
Consider a form with an address section that appears in multiple places: user registration, company profiles, shipping details. You want to write the address fields once and reuse them.
import { } from "zod";
const = .({
: .().(1),
: .().(1),
: .().(1),
});
const = .({
: .(),
: ,
});
const = .({
: .(),
: ,
});Both forms have an address field with the same structure. Let's make that reusable.
Dynamic Inheritance
The simplest approach uses useFormInstance with inherit: true. The component reads its position from context and works wherever it's placed.
// AddressFields.tsx
function () {
const { } = <>({ : true });
return (
<>
< ="street" ="Street">
< />
</>
< ="city" ="City">
< />
</>
< ="zipCode" ="Zip Code">
< />
</>
</>
);
}
// UserForm.tsx
function () {
const { , } = ({
: ,
: () => .(),
});
return (
< ="vertical">
< ="name" ="Name">
< />
</>
{/* AddressFields inherits the "address" prefix */}
< ="address">
< />
</>
< ="primary" ="submit">
Submit
</>
</>
);
}When AddressFields renders inside <FormItem name="address">, its fields automatically get prefixed. The street field becomes address.street in the form data.
How It Works
- The parent
FormItemwithname="address"sets a prefix in context useFormInstance({ inherit: true })reads that prefix- Child
FormItemcomponents prepend the prefix to theirnameprop - The final form data has the correct nested structure
Explicit Paths with createFormHook
For stricter type safety, use createFormHook to generate typed hooks for a specific schema. This catches errors when the component is used in the wrong context.
import React from "react";
import { z } from "zod";
import { createFormHook } from "antd-typed";
import { Input, Button, Form } from "antd";
const userSchema = z.object({
name: z.string(),
email: z.string().email(),
address: z.object({
street: z.string(),
city: z.string(),
}),
});
// Create typed hooks for this schema
const { useCustomForm, useCustomFormInstance } = createFormHook({
validator: userSchema,
});
// AddressSection knows it operates on userSchema.address
function AddressSection() {
const { FormItem } = useCustomFormInstance({ inheritAt: ["address"] });
return (
<>
<FormItem name="street" label="Street">
<Input />
</FormItem>
<FormItem name="city" label="City">
<Input />
</FormItem>
</>
);
}
function UserForm() {
const { Form, FormItem } = useCustomForm({
onFinish: (values) => console.log(values),
});
return (
<Form layout="vertical">
<FormItem name="name" label="Name">
<Input />
</FormItem>
<FormItem name="email" label="Email">
<Input />
</FormItem>
<AddressSection />
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form>
);
}With inheritAt: ["address"], the FormItem in AddressSection only accepts field names that exist under address in the schema. Using name="name" would be a type error.
When to Use Which
| Approach | Use When |
|---|---|
inherit: true | Component is generic and reused across different forms |
inheritAt: [...] | Component is tied to a specific form schema |
The dynamic approach is more flexible. The explicit approach catches more errors at compile time.
Nested Composition
Components can nest arbitrarily deep. Each level adds to the prefix.
import React from "react";
import { z } from "zod";
import { useForm, useFormInstance } from "antd-typed";
import { Input, Form } from "antd";
const schema = z.object({
company: z.object({
name: z.string(),
headquarters: z.object({
street: z.string(),
city: z.string(),
}),
}),
});
type Headquarters = { street: string; city: string };
type Company = { name: string; headquarters: Headquarters };
// ---cut---
function HeadquartersFields() {
const { FormItem } = useFormInstance<Headquarters>({ inherit: true });
return (
<>
<FormItem name="street" label="Street">
<Input />
</FormItem>
<FormItem name="city" label="City">
<Input />
</FormItem>
</>
);
}
function CompanyFields() {
const { FormItem } = useFormInstance<Company>({ inherit: true });
return (
<>
<FormItem name="name" label="Company Name">
<Input />
</FormItem>
<FormItem name="headquarters">
<HeadquartersFields />
</FormItem>
</>
);
}
function CompanyForm() {
const { Form, FormItem } = useForm({ validator: schema });
return (
<Form layout="vertical">
<FormItem name="company">
<CompanyFields />
</FormItem>
</Form>
);
}The street field ends up at company.headquarters.street in the form data.
Accessing the Form Instance
Both approaches give you access to the underlying form instance for imperative operations.
import React from "react";
import { } from "antd-typed";
type = { : string; : string };
function () {
const { , } = <>({ : true });
const = () => {
.({
: "123 Main St",
: "Springfield",
});
};
return (
<>
< ={}>Fill Example</>
{/* ... FormItems ... */}
</>
);
}