Frontend Development 21 min read

From HOC to Hooks: How Ant Design Form Works Under the Hood

This article explores the inner workings of Ant Design Form, comparing the legacy HOC‑based implementation with the modern Hooks API, detailing state‑lifting, form store design, performance trade‑offs, and providing step‑by‑step code examples for building a custom form solution in React.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
From HOC to Hooks: How Ant Design Form Works Under the Hood

Ant Design Form Overview

As a front‑end developer, forms are the most frequently used component, and Ant Design Form is the go‑to library for React developers. From simple login dialogs to complex data‑entry pages, it appears everywhere. This article dives into its implementation, tracing the evolution from the HOC‑based

Form.create()

API to the Hooks‑based

Form.useForm()

API.

3.x Version – HOC Pattern

Design Idea

In version 3.x, Ant Design Form uses a state‑lifting strategy: form controls and the submit button share a common ancestor that stores shared state (values and errors). This enables cross‑component state sharing, unified validation logic, and centralized data flow.

Cross‑component state sharing

Unified validation logic

Centralized data flow

HOC Pattern Explained

The core implementation is a Higher‑Order Component (HOC). It is a function that takes a component and returns a new component, achieving logic decoupling, reusability, and state isolation.

Logic decoupling : separates form logic from UI rendering

Reusability : the same form logic can be reused across components

State isolation : each form maintains its own context

Practical Demo – Simplified HOC Form

<code>const nameRules = { required: true, message: "请输入用户名!" };
const passwordRules = { required: true, message: "请输入密码!" };

@Form.create()
class MyForm extends Component {
  componentDidMount() {
    this.props.form.setFieldsValue({ username: "小明" });
  }
  submit = () => {
    const { getFieldsValue, validateFields } = this.props.form;
    console.log("submit", getFieldsValue());
    validateFields((err, val) => {
      if (err) console.log("err", err);
      else console.log("校验成功", val);
    });
  };
  render() {
    const { getFieldDecorator } = this.props.form;
    return (
      <div>
        <h3>MyForm</h3>
        {getFieldDecorator("username", { rules: [nameRules] })(<Input placeholder="Username" />)}
        {getFieldDecorator("password", { rules: [passwordRules] })(<Input placeholder="Password" />)}
        <button onClick={this.submit}>submit</button>
      </div>
    );
  }
}
export default MyForm;
</code>

Hand‑written Form.create()

To replace the built‑in API, we can implement our own HOC:

<code>import React, { Component } from "react";

export default function createForm(WrappedComponent) {
  return class extends Component {
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}
</code>

The HOC stores form state in its own

state

object and injects three key props into wrapped inputs via

React.cloneElement

:

name : field identifier

value : value read from HOC state

onChange : unified change handler

<code>handleChange = e => {
  this.setState({ [e.target.name]: e.target.value });
};

getFieldDecorator = (field, options) => InputComponent => {
  return React.cloneElement(InputComponent, {
    name: field,
    value: this.state[field],
    onChange: this.handleChange,
  });
};
</code>

Validation iterates over stored field options and reports errors:

<code>validateFields = callback => {
  let error = {};
  for (let field in this.options) {
    if (!this.state[field]) {
      error[field] = this.options[field].required;
    }
  }
  if (Object.keys(error).length > 0) callback(error, this.state);
  else callback(null, this.state);
};
</code>

Performance Drawbacks of HOC

Because all field values live in a single state object, any change triggers a re‑render of the entire HOC tree, causing even untouched fields to re‑render. This defeats component‑level optimizations such as

React.memo

or

PureComponent

, and deepens the component hierarchy, making debugging harder.

4.x/5.x Version – Hooks API

Design Idea

The Hooks implementation moves state management out of the component tree into an independent

FormStore

that follows a publish‑subscribe pattern, similar to Redux. The store holds field values and a list of subscribed items, notifying them only when their specific data changes.

FormStore architecture diagram
FormStore architecture diagram

Practical Demo – Simplified Hooks Form

<code>import React, { Component } from "react";
import { Form, Button, Input } from "antd";

const FormItem = Form.Item;
const nameRules = { required: true, message: "请输入姓名!" };
const passworRules = { required: true, message: "请输入密码!" };

export default function MyForm(props) {
  const [form] = Form.useForm();
  const onFinish = val => console.log("onFinish", val);
  const onFinishFailed = val => console.log("onFinishFailed", val);
  return (
    <div>
      <h3>AntdFormPage</h3>
      <Form ref={formRef} onFinish={onFinish} onFinishFailed={onFinishFailed}>
        <FormItem rules={[nameRules]}>
          <Input placeholder="username placeholder" />
        </FormItem>
        <FormItem rules={[passworRules]}>
          <Input placeholder="password placeholder" />
        </FormItem>
        <FormItem>
          <Button type="primary" size="large" htmlType="submit">Submit</Button>
        </FormItem>
      </Form>
    </div>
  );
}
</code>

Architecture Layout

<code>/my-form
├── FormContext.js    // context for passing form instance
├── Form.js          // form container component
├── Item.js          // Form.Item component
├── useForm.js       // form store logic
└── index.js         // export entry
</code>

FormContext

<code>import { createContext } from "react";
const FormContext = createContext();
export default FormContext;
</code>

Form Component

<code>import React from "react";
import FormContext from "./FormContext";
import useForm from "./useForm";

export default function Form({ children, form }) {
  const [formInstance] = useForm(form);
  return (
    <form>
      <FormContext.Provider value={formInstance}>
        {children}
      </FormContext.Provider>
    </form>
  );
}
</code>

Item Component (Controlled Input)

<code>import React, { useContext } from "react";
import FormContext from "./FormContext";

export default function Item({ children, name }) {
  const formInstance = useContext(FormContext);
  const { getFieldValue, setFieldsValue } = formInstance;
  const getControlled = () => ({
    value: getFieldValue(name),
    onChange: e => {
      const newValue = e.target.value;
      setFieldsValue({ [name]: newValue });
    },
  });
  return React.cloneElement(children, getControlled());
}
</code>

FormStore (useForm Hook)

<code>class FormStore {
  constructor() {
    this.store = {};
    this.itemEntities = [];
  }
  registerItemEntities = entity => {
    this.itemEntities.push(entity);
    return () => {
      this.itemEntities = this.itemEntities.filter(item => item !== entity);
      delete this.store[entity.props.name];
    };
  };
  getFieldsValue = () => ({ ...this.store });
  getFieldValue = name => this.store[name];
  setFieldsValue = newStore => {
    this.store = { ...this.store, ...newStore };
    this.itemEntities.forEach(entity => {
      if (Object.keys(newStore).includes(entity.props.name)) {
        entity.onStoreChange();
      }
    });
  };
  validateFields = callback => {
    const error = {};
    this.itemEntities.forEach(entity => {
      const { name, rules } = entity.props;
      const value = this.getFieldValue(name);
      const rule = rules && rules[0];
      if (rule && rule.required && (value === undefined || value === "")) {
        error[name] = rule.message;
      }
    });
    if (Object.keys(error).length) callback(error, this.store);
    else callback(null, this.store);
  };
  getForm = () => ({
    getFieldsValue: this.getFieldsValue,
    getFieldValue: this.getFieldValue,
    setFieldsValue: this.setFieldsValue,
    validateFields: this.validateFields,
    registerItemEntities: this.registerItemEntities,
  });
}

export default function useForm(form) {
  const formRef = React.useRef();
  if (!formRef.current) {
    const formStore = new FormStore();
    formRef.current = formStore.getForm();
  }
  return [formRef.current];
}
</code>

Form Submission

<code>export default function Form({ children, form, onFinish, onFinishFailed }, ref) {
  const [formInstance] = useForm(form);
  formInstance.setCallbacks({ onFinish, onFinishFailed });
  return (
    <form onSubmit={e => { e.preventDefault(); formInstance.submit(); }}>
      <FormContext.Provider value={formInstance}>
        {children}
      </FormContext.Provider>
    </form>
  );
}
</code>

Conclusion

Both HOC and Hooks implementations embody valuable design insights. While HOC is gradually replaced by Hooks in modern React, understanding its decorator pattern deepens comprehension of state‑management libraries such as Redux or Zustand, and explains why React 18 introduced

useSyncExternalStore

for concurrent rendering scenarios.

State ManagementReactHooksant-designFormHOC
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.