Mastering React Forms: From Official APIs to Advanced Community Solutions

This article provides a comprehensive analysis of form handling in React, covering the fundamentals of forms, the official controlled and uncontrolled approaches, and a detailed comparison of major community libraries such as rc-form, rc-field-form, Formik, React‑final‑form, react‑hook‑form, and Formily, including code examples, performance considerations, and practical usage tips.

ByteFE
ByteFE
ByteFE
Mastering React Forms: From Official APIs to Advanced Community Solutions

1. Concept of Forms

In web development a form is a UI component that collects user input and converts it into a structured data object, usually a JSON payload. Typical responsibilities include rendering appropriate input components (e.g., text fields, selects), styling labels and error messages, performing validation (including dependent validation), representing nested data structures (objects, arrays), and handling inter‑field dependencies.

{
  "lastName": "韩",
  "firstName": "红"
}

2. React Official Form Solutions

React’s documentation describes two basic patterns:

2.1 Controlled Component

The parent component fully manages the field state via value and onChange. The child receives the value as a prop and notifies the parent on change.

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = { value: '' };
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }
  handleChange(event) {
    this.setState({ value: event.target.value });
  }
  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

2.2 Uncontrolled Component

The component stores its own state and exposes the value through a ref. The parent reads the value from the ref when needed.

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.input = React.createRef();
  }
  handleSubmit(event) {
    alert('A name was submitted: ' + this.input.current.value);
    event.preventDefault();
  }
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={this.input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

2.3 Choosing Between Controlled and Uncontrolled

Controlled components provide a predictable data flow but can be verbose. Uncontrolled components rely on refs and can achieve the same behaviours (e.g., field‑level validation) when combined with custom event handling. The choice is therefore nuanced and depends on the specific use case.

3. Community Form Libraries

Because the official APIs are minimal, the React ecosystem offers many third‑party form solutions. The following sections highlight the most representative libraries, their design goals, API differences, and performance characteristics.

3.1 rc‑form (Ant Design Form 3.x)

rc‑form is the underlying implementation of Ant Design Form 3.x. It uses a higher‑order component ( getFieldDecorator) to inject value and onChange into wrapped fields. The library triggers a global forceUpdate on any field change, causing the entire form to re‑render and leading to performance bottlenecks.

import { Form, Icon, Input, Button } from 'antd';
function hasErrors(fieldsError) {
  return Object.keys(fieldsError).some(field => fieldsError[field]);
}
class HorizontalLoginForm extends React.Component {
  componentDidMount() { this.props.form.validateFields(); }
  handleSubmit = e => {
    e.preventDefault();
    this.props.form.validateFields((err, values) => {
      if (!err) console.log('Received values of form:', values);
    });
  };
  render() {
    const { getFieldDecorator, getFieldsError, getFieldError, isFieldTouched } = this.props.form;
    const usernameError = isFieldTouched('username') && getFieldError('username');
    const passwordError = isFieldTouched('password') && getFieldError('password');
    return (
      <Form layout="inline" onSubmit={this.handleSubmit}>
        <Form.Item validateStatus={usernameError ? 'error' : ''} help={usernameError || ''}>
          {getFieldDecorator('username', { rules: [{ required: true, message: 'Please input your username!' }] }) (
            <Input prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} placeholder="Username" />
          )}
        </Form.Item>
        <Form.Item validateStatus={passwordError ? 'error' : ''} help={passwordError || ''}>
          {getFieldDecorator('password', { rules: [{ required: true, message: 'Please input your Password!' }] }) (
            <Input prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} type="password" placeholder="Password" />
          )}
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit" disabled={hasErrors(getFieldsError())}>Log in</Button>
        </Form.Item>
      </Form>
    );
  }
}
const WrappedHorizontalLoginForm = Form.create({ name: 'horizontal_login' })(HorizontalLoginForm);
ReactDOM.render(<WrappedHorizontalLoginForm />, mountNode);

3.2 rc‑field‑form (Ant Design Form 4.x)

rc‑field‑form replaces the global re‑render with a subscription model. Each field subscribes only to the slice of state it cares about, dramatically reducing unnecessary renders. The API is simplified; getFieldDecorator is removed.

import { Form, Input, Button, Checkbox } from 'antd';
const layout = { labelCol: { span: 8 }, wrapperCol: { span: 16 } };
const tailLayout = { wrapperCol: { offset: 8, span: 16 } };
const Demo = () => {
  const onFinish = values => console.log('Success:', values);
  const onFinishFailed = errorInfo => console.log('Failed:', errorInfo);
  return (
    <Form {...layout} name="basic" onFinish={onFinish} onFinishFailed={onFinishFailed}>
      <Form.Item label="Username" name="username" rules={[{ required: true, message: 'Please input your username!' }]}>
        <Input />
      </Form.Item>
      <Form.Item label="Password" name="password" rules={[{ required: true, message: 'Please input your password!' }]}>
        <Input.Password />
      </Form.Item>
      <Form.Item name="remember" valuePropName="checked" {...tailLayout}>
        <Checkbox>Remember me</Checkbox>
      </Form.Item>
      <Form.Item {...tailLayout}>
        <Button type="primary" htmlType="submit">Submit</Button>
      </Form.Item>
    </Form>
  );
};
ReactDOM.render(<Demo />, mountNode);

3.3 Redux‑Form

Early React form library that stores form state in Redux. Versions < v6 trigger a full form re‑render on every field change. From v6 onward each field registers separately and leverages Redux’s subscription mechanism for fine‑grained updates.

3.4 Formik

Formik manages form state internally (no Redux) and provides FastField for field‑level memoization. The overall update model is still coarse‑grained, so large or highly dynamic forms may still suffer from full‑form renders.

3.5 React‑final‑form

Created by the author of Redux‑Form, this library is framework‑agnostic and uses a subscription model similar to rc‑field‑form. Each field independently subscribes to the parts of the state it needs, providing efficient updates without extra dependencies.

3.6 React‑hook‑form

Adopts an uncontrolled‑component mindset: each input registers a ref and the library reads values directly from the DOM, avoiding state‑driven re‑renders. It supports dynamic fields via Form.List, provides watch and useWatch for fine‑grained subscriptions, and offers a Controller component to integrate with controlled UI libraries such as Ant Design.

3.7 Nested Data Support in rc‑field‑form

Field names can be arrays (e.g., ["user", "name"]) which are flattened into a nested JSON structure on submit. This enables representation of complex objects and arrays without custom parsing.

import { Form, Input, InputNumber, Button } from 'antd';
const Demo = () => {
  const onFinish = values => console.log(values);
  return (
    <Form name="nest-messages" onFinish={onFinish}>
      <Form.Item name={['user', 'name']} label="Name" rules={[{ required: true }]}>
        <Input />
      </Form.Item>
      <Form.Item name={['user', 'email']} label="Email" rules={[{ type: 'email' }]}>
        <Input />
      </Form.Item>
      <Form.Item name={['user', 'age']} label="Age" rules={[{ type: 'number', min: 0, max: 99 }]}>
        <InputNumber />
      </Form.Item>
      <Form.Item name={['list', 0]} label="address1">
        <Input />
      </Form.Item>
      <Form.Item name={['list', 1]} label="address2">
        <Input.TextArea />
      </Form.Item>
      <Form.Item>
        <Button type="primary" htmlType="submit">Submit</Button>
      </Form.Item>
    </Form>
  );
};
ReactDOM.render(<Demo />, document.getElementById('container'));

3.8 Dynamic Arrays with Form.List (rc‑field‑form)

rc‑field‑form provides Form.List to add or remove items in an array field. Each item receives a unique key and a name path.

import { Form, Input, Button, Space } from 'antd';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
const Demo = () => (
  <Form>
    <Form.List name="sights">
      {(fields, { add, remove }) => (
        <>
          {fields.map(field => (
            <Space key={field.key} align="baseline">
              <Form.Item {...field} label="Price" name={[field.name, 'price']} fieldKey={[field.fieldKey, 'price']} rules={[{ required: true, message: 'Missing price' }]}>
                <Input />
              </Form.Item>
              <MinusCircleOutlined onClick={() => remove(field.name)} />
            </Space>
          ))}
          <Form.Item>
            <Button type="dashed" onClick={() => add()} block icon=<PlusOutlined />>
              Add sight
            </Button>
          </Form.Item>
        </>
      )}
    </Form.List>
    <Form.Item>
      <Button type="primary" htmlType="submit">Submit</Button>
    </Form.Item>
  </Form>
);
ReactDOM.render(<Demo />, document.getElementById('container'));

3.9 Horizontal Comparison

All libraries converge on the same core problem: efficient communication between fields and the form container. Early solutions (rc‑form, Redux‑Form v5) used full‑form re‑renders. Later solutions (rc‑field‑form, React‑final‑form, React‑hook‑form) adopt a subscription model where each field only updates when the slice of state it cares about changes.

4. Formily (1.x)

4.1 Background

Formily targets scenarios with heavy inter‑field dependencies, large nested data structures, and strict performance requirements. It treats the entire form as a tree‑structured model and provides a declarative schema, fine‑grained subscriptions, and an effects system built on RxJS.

4.2 Core Ideas

All form logic is centralized in an effects array, which declares event‑driven reactions using RxJS streams.

The underlying data model is fully observable; every field’s value, error, and UI state are part of a subscribable tree.

Immer is used to generate patches, enabling efficient immutable updates without manual diffing.

4.3 Schema & Low‑Code

Formily extends JSON Schema to describe form structure, validation rules, and UI components. This enables visual form builders and code‑generation pipelines. The schema can be written as JSON, JSX, or pure JSX, and the library guarantees that all representations produce the same runtime form.

4.4 Communication Model

Formily’s effects act as a single source of truth for side‑effects and field interactions. An effect is expressed as $(eventType, pathRule), which selects all fields matching pathRule and subscribes to eventType. The result is an RxJS observable that can trigger state updates, validation, visibility changes, etc. Because the whole form state is observable, effects can manipulate any field property (value, rules, UI state) without needing additional React hooks.

4.5 Performance Techniques

Fine‑grained subscription ensures only the fields that depend on a changed value re‑render.

Immer patches record the exact mutations, avoiding full‑object cloning.

Proxy‑wrapped internal state tracks which properties are accessed; unchanged subscriptions are ignored.

5. Conclusion

The evolution of React form handling moves from global re‑render strategies (rc‑form, early Redux‑Form) to subscription‑based, field‑level updates (rc‑field‑form, React‑final‑form, React‑hook‑form). Modern libraries minimize unnecessary renders by letting each field subscribe only to the data it needs. Formily pushes the envelope further with a schema‑first, effect‑driven architecture that centralizes all form logic, provides a fully observable tree model, and leverages Immer and RxJS for high‑performance updates in complex, highly‑dynamic forms.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ReactFormilyReact Hook Formrc-formFormControlled ComponentsFormikUncontrolled Components
ByteFE
Written by

ByteFE

Cutting‑edge tech, article sharing, and practical insights from the ByteDance frontend team.

0 followers
Reader feedback

How this landed with the community

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.