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.
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
stateobject 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.memoor
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
FormStorethat 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.
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
useSyncExternalStorefor concurrent rendering scenarios.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.