antd-typed

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

  1. The parent FormItem with name="address" sets a prefix in context
  2. useFormInstance({ inherit: true }) reads that prefix
  3. Child FormItem components prepend the prefix to their name prop
  4. 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

ApproachUse When
inherit: trueComponent 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 ... */}
    </>
  );
}

On this page