• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

大家写中后台系统的时候,应该都用过 Ant Design 的 Form 组件:

用 Form.Item 包裹 Input、Checkbox 等表单项,可以定义 rules,也就是每个表单项的校验规则。

外层 Form 定义 initialValues 初始值,onFinish 当提交时的回调,onFinishFailed 当提交有错误时的回调。

Form 组件每天都在用,那它是怎么实现的呢?

其实原理不复杂。

每个表单项都有 value 和 onChange 参数,我们只要在 Item 组件里给 children 传入这俩参数,把值收集到全局的 Store 里。

这样在 Store 里就存储了所有表单项的值,在 submit 时就可以取出来传入 onFinish 回调。

并且,还可以用 async-validator 对表单项做校验,如果有错误,就把错误收集起来传入 onFinishFailed 回调。

那这些 Item 是怎么拿到 Store 来同步表单值的呢?

用 Context。

在 Form 里保存 Store 到 Context,然后在 Item 里取出 Context 的 Store 来,同步表单值到 Store。

我们来写下试试:

npx create-vite

安装依赖,改下 main.tsx

然后创建 Form/FormContext.ts

import { createContext } from "react";

export interface FormContextProps {
    values?: Record<string, any>;
    setValues?: (values: Record<string, any>) => void;
    onValueChange?: (key: string, value: any) => void;
    validateRegister?: (name: string, cb: Function) => void;
}

export default createContext<FormContextProps>({});

在 context 里保存 values 也就是 Store 的值。

然后添加 setValues 来修改 values

onValueChange 监听 value 变化

validateRegister 用来注册表单项的校验规则,也就是 rules 指定的那些。

然后写下 Form 组件 Form/Form.tsx

参数传入初始值 initialValues、点击提交的回调 onFinish、点击提交有错误时的回调 onFinishFailed。

这里的 Record<string,any> 是 ts 的类型,任意的对象的意思。

用 useState 保存 values,用 useRef 保存 errors 和 validator

为什么不都用 useState 呢?

因为修改 state 调用 setState 的时候会触发重新渲染。

而 ref 的值保存在 current 属性上,修改它不会触发重新渲染。

errors、validator 这种就是不需要触发重新渲染的数据。

然后 onValueChange 的时候就是修改 values 的值。

submit 的时候调用 onFinish,传入 values,再调用所有 validator 对值做校验,如果有错误,调用 onFinishFailed 回调:

然后把这些方法保存到 context 中,并且给原生 form 元素添加 onSubmit 的处理:

import React, {
    CSSProperties,
    useState,
    useRef,
    FormEvent,
    ReactNode,
} from "react";
import classNames from "classnames";
import FormContext from "./FormContext";

export interface FormProps extends React.HTMLAttributes<HTMLFormElement> {
    className?: string;
    style?: CSSProperties;
    onFinish?: (values: Record<string, any>) => void;
    onFinishFailed?: (errors: Record<string, any>) => void;
    initialValues?: Record<string, any>;
    children?: ReactNode;
}

const Form = (props: FormProps) => {
    const {
        className,
        style,
        children,
        onFinish,
        onFinishFailed,
        initialValues,
        ...others
    } = props;

    const [values, setValues] = useState<Record<string, any>>(
        initialValues || {}
    );

    const validatorMap = useRef(new Map<string, Function>());

    const errors = useRef<Record<string, any>>({});

    const onValueChange = (key: string, value: any) => {
        values[key] = value;
    };

    const handleSubmit = (e: FormEvent) => {
        e.preventDefault();

        for (let [key, callbackFunc] of validatorMap.current) {
            if (typeof callbackFunc === "function") {
                errors.current[key] = callbackFunc();
            }
        }

        const errorList = Object.keys(errors.current)
            .map((key) => {
                return errors.current[key];
            })
            .filter(Boolean);

        if (errorList.length) {
            onFinishFailed?.(errors.current);
        } else {
            onFinish?.(values);
        }
    };

    const handleValidateRegister = (name: string, cb: Function) => {
        validatorMap.current.set(name, cb);
    };

    const cls = classNames("ant-form", className);

    return (
        <FormContext.Provider
            value={{
                onValueChange,
                values,
                setValues: (v) => setValues(v),
                validateRegister: handleValidateRegister,
            }}>
            <form
                {...others}
                className={cls}
                style={style}
                onSubmit={handleSubmit}>
                {children}
            </form>
        </FormContext.Provider>
    );
};

export default Form;

这里用到了 classnames 包要安装下:

npm install --save classnames

接下来添加 Form/Item.tsx,也就是包装表单项用的组件:

首先是参数,可以传入 label、name、valuePropName、rules 等:

valuePropName 默认是 value,当 checkbox 等表单项就要取 checked 属性了:

这里 children 类型为 ReactElement 而不是 ReactNode。

因为 ReactNode 除了包含 ReactElement 外,还有 string、number 等:

而作为 Form.Item 组件的 children,只能是 ReactElement。

然后实现下 Item 组件:

如果没有传入 name 参数,那就直接返回 children。

比如这种就不需要包装:

创建两个 state,分别存储表单值 value 和 error。

从 context 中读取对应 name 的 values 的值,同步设置 value:

然后 React.cloneElement 复制 chilren,额外传入 value、onChange 等参数:

onChange 回调里设置 value,并且修改 context 里的 values 的值:

这里的 getValueFromEvent 是根据表单项类型来获取 value:

然后是校验 rules,这个是用 async-validator 这个包:

在 context 注册 name 对应的 validator 函数:

然后 Item 组件渲染 label、children、error

import React, {
    ReactNode,
    CSSProperties,
    useState,
    useContext,
    ReactElement,
    useEffect,
    PropsWithChildren,
    ChangeEvent,
} from "react";
import classNames from "classnames";
import Schema, { Rules } from "async-validator";

import FormContext from "./FormContext";

export interface ItemProps {
    className?: string;
    style?: CSSProperties;
    label?: ReactNode;
    name?: string;
    valuePropName?: string;
    rules?: Array<Record<string, any>>;
    children?: ReactElement;
}

const getValueFromEvent = (e: ChangeEvent<HTMLInputElement>) => {
    const { target } = e;
    if (target.type === "checkbox") {
        return target.checked;
    } else if (target.type === "radio") {
        return target.value;
    }

    return target.value;
};

const Item = (props: ItemProps) => {
    const { className, label, children, style, name, valuePropName, rules } =
        props;

    if (!name) {
        return children;
    }

    const [value, setValue] = useState<string | number | boolean>();
    const [error, setError] = useState("");

    const { onValueChange, values, validateRegister } = useContext(FormContext);

    useEffect(() => {
        if (value !== values?.[name]) {
            setValue(values?.[name]);
        }
    }, [values, values?.[name]]);

    const handleValidate = (value: any) => {
        let errorMsg = null;
        if (Array.isArray(rules) && rules.length) {
            const validator = new Schema({
                [name]: rules.map((rule) => {
                    return {
                        type: "string",
                        ...rule,
                    };
                }),
            });

            validator.validate({ [name]: value }, (errors) => {
                if (errors) {
                    if (errors?.length) {
                        setError(errors[0].message!);
                        errorMsg = errors[0].message;
                    }
                } else {
                    setError("");
                    errorMsg = null;
                }
            });
        }

        return errorMsg;
    };

    useEffect(() => {
        validateRegister?.(name, () => handleValidate(value));
    }, [value]);

    const propsName: Record<string, any> = {};
    if (valuePropName) {
        propsName[valuePropName] = value;
    } else {
        propsName.value = value;
    }

    const childEle =
        React.Children.toArray(children).length > 1
            ? children
            : React.cloneElement(children!, {
                  ...propsName,
                  onChange: (e: ChangeEvent<HTMLInputElement>) => {
                      const value = getValueFromEvent(e);
                      setValue(value);
                      onValueChange?.(name, value);

                      handleValidate(value);
                  },
              });

    const cls = classNames("ant-form-item", className);

    return (
        <div className={cls} style={style}>
            <div>{label && <label>{label}</label>}</div>
            <div>
                {childEle}
                {error && <div style={{ color: "red" }}>{error}</div>}
            </div>
        </div>
    );
};

export default Item;

安装用到的 async-validator:

npm install --save async-validator

然后在 Form/index.tsx 导出下:

import InternalForm from "./Form";
import Item from "./Item";

type InternalFormType = typeof InternalForm;

interface FormInterface extends InternalFormType {
    Item: typeof Item;
}

const Form = InternalForm as FormInterface;

Form.Item = Item;

export default Form;

主要是把 Item 挂在 Form 下。

在 App.tsx 测试下:

import { Button, Checkbox, Input } from "antd";
import Form from "./Form/index";

const Basic: React.FC = () => {
    const onFinish = (values: any) => {
        console.log("Success:", values);
    };

    const onFinishFailed = (errorInfo: any) => {
        console.log("Failed:", errorInfo);
    };

    return (
        <Form
            initialValues={{ remember: true, username: "神说要有光" }}
            onFinish={onFinish}
            onFinishFailed={onFinishFailed}>
            <Form.Item
                label="Username"
                name="username"
                rules={[
                    { required: true, message: "请输入用户名!" },
                    { max: 6, message: "长度不能大于 6" },
                ]}>
                <Input />
            </Form.Item>

            <Form.Item
                label="Password"
                name="password"
                rules={[{ required: true, message: "请输入密码!" }]}>
                <Input.TextArea />
            </Form.Item>

            <Form.Item name="remember" valuePropName="checked">
                <Checkbox>记住我</Checkbox>
            </Form.Item>

            <Form.Item>
                <div>
                    <Button type="primary" htmlType="submit">
                        登录
                    </Button>
                </div>
            </Form.Item>
        </Form>
    );
};

export default Basic;

除了 Form 外,具体表单项用的 antd 的组件。

试一下:

form 的 initialValues 的设置、表单的值的保存,规则的校验和错误显示,都没问题。

这样,Form 组件的核心功能就完成了。

核心就是一个 Store 来保存表单的值,然后用 Item 组件包裹具体表单,设置 value 和 onChange 来同步表单的值。

当值变化以及 submit 的时候用 async-validator 来校验。

那 antd 的 Form 也是这样实现的么?

基本是一样的。

我们来看下源码:

antd 的 Form 有个叫 FormStore 的类:

它的 store 属性保存表单值,然后暴露 getFieldValue、setFieldValue 等方法来读写 store。

然后它提供了一个 useForm 的 hook 来创建 store:

用的时候这样用:

这样,Form 组件里就可以通过传进来的 store 的 api 来读写 store 了:

当然,它会通过 context 把 store 传递下去:

在 Field 也就是 Item 组件里就通过 context 取出 store 的 api 来读写 store:

和我们的实现有区别么?

有点区别,antd 的 FormStore 是可以独立出来的,通过 useForm 创建好传入 Form 组件。

而我们的 Store 没有分离出来,直接内置在 Form 组件里了。

但是实现的思路都是一样的。

提供个 useForm 的 api 的好处是,外界可以拿到 store 的 api 来自己修改 store。

当然,我们也可以通过 ref 来做这个:

import React, {
    CSSProperties,
    useState,
    useRef,
    FormEvent,
    ReactNode,
    ForwardRefRenderFunction,
    useImperativeHandle,
    forwardRef,
} from "react";
import classNames from "classnames";
import FormContext from "./FormContext";

export interface FormProps extends React.HTMLAttributes<HTMLFormElement> {
    className?: string;
    style?: CSSProperties;
    onFinish?: (values: Record<string, any>) => void;
    onFinishFailed?: (errors: Record<string, any>) => void;
    initialValues?: Record<string, any>;
    children?: ReactNode;
}

export interface FormRefApi {
    getFieldsValue: () => Record<string, any>;
    setFieldsValue: (values: Record<string, any>) => void;
}

const Form = forwardRef<FormRefApi, FormProps>((props: FormProps, ref) => {
    const {
        className,
        style,
        children,
        onFinish,
        onFinishFailed,
        initialValues,
        ...others
    } = props;

    const [values, setValues] = useState<Record<string, any>>(
        initialValues || {}
    );

    useImperativeHandle(ref, () => {
        return {
            getFieldsValue() {
                return values;
            },
            setFieldsValue(fieldValues) {
                setValues({ ...values, ...fieldValues });
            },
        };
    }, []);

    const validatorMap = useRef(new Map<string, Function>());

    const errors = useRef<Record<string, any>>({});

    const onValueChange = (key: string, value: any) => {
        values[key] = value;
    };

    const handleSubmit = (e: FormEvent) => {
        e.preventDefault();

        for (let [key, callbackFunc] of validatorMap.current) {
            if (typeof callbackFunc === "function") {
                errors.current[key] = callbackFunc();
            }
        }

        const errorList = Object.keys(errors.current)
            .map((key) => {
                return errors.current[key];
            })
            .filter(Boolean);

        if (errorList.length) {
            onFinishFailed?.(errors.current);
        } else {
            onFinish?.(values);
        }
    };

    const handleValidateRegister = (name: string, cb: Function) => {
        validatorMap.current.set(name, cb);
    };

    const cls = classNames("ant-form", className);

    return (
        <FormContext.Provider
            value={{
                onValueChange,
                values,
                setValues: (v) => setValues(v),
                validateRegister: handleValidateRegister,
            }}>
            <form
                {...others}
                className={cls}
                style={style}
                onSubmit={handleSubmit}>
                {children}
            </form>
        </FormContext.Provider>
    );
});

export default Form;

然后在 App.tsx 试试:

import { Button, Checkbox, Input } from "antd";
import Form from "./Form/index";
import { useEffect, useRef } from "react";
import { FormRefApi } from "./Form/Form";

const Basic: React.FC = () => {
    const onFinish = (values: any) => {
        console.log("Success:", values);
    };

    const onFinishFailed = (errorInfo: any) => {
        console.log("Failed:", errorInfo);
    };

    const form = useRef<FormRefApi>(null);

    return (
        <>
            <Button
                type="primary"
                onClick={() => {
                    console.log(form.current?.getFieldsValue());
                }}>
                打印表单值
            </Button>

            <Button
                type="primary"
                onClick={() => {
                    form.current?.setFieldsValue({
                        username: "东东东",
                    });
                }}>
                设置表单值
            </Button>

            <Form
                ref={form}
                initialValues={{ remember: true, username: "神说要有光" }}
                onFinish={onFinish}
                onFinishFailed={onFinishFailed}>
                <Form.Item
                    label="Username"
                    name="username"
                    rules={[
                        { required: true, message: "请输入用户名!" },
                        { max: 6, message: "长度不能大于 6" },
                    ]}>
                    <Input />
                </Form.Item>

                <Form.Item
                    label="Password"
                    name="password"
                    rules={[{ required: true, message: "请输入密码!" }]}>
                    <Input.TextArea />
                </Form.Item>

                <Form.Item name="remember" valuePropName="checked">
                    <Checkbox>记住我</Checkbox>
                </Form.Item>

                <Form.Item>
                    <div>
                        <Button type="primary" htmlType="submit">
                            登录
                        </Button>
                    </div>
                </Form.Item>
            </Form>
        </>
    );
};

export default Basic;

当然,你也可以把 store 的 api 处理出来,然后封装个 useForm 的 hook 来传入 Form 组件。

这样,用法比 ref 的方式简单点。

至此,我们就实现了 antd 的 Form 的功能。

案例代码上传了 react 小册仓库

总结

我们每天都在用 antd 的 Form 组件,今天自己实现了下。

其实原理不复杂,就是把 Form 的表单项的值存储到 Store 中。

在 Form 组件里把 Store 放到 Context,在 Item 组件里取出来。

用 Item 组件包裹表单项,传入 value、onChange 参数用来同步表单值到 Store。

这样,表单项的值变化或者 submit 的时候,就可以根据 rules 用 async-validator 来校验。

此外,我们还通过 ref 暴露出了 setFieldsValue、getFieldsValue 等 store 的 api。

当然,在 antd 的 Form 里是通过 useForm 这个 hook 来创建 store,然后把它传入 Form 组件来用的。

两种实现方式都可以。

每天都用 antd 的 Form 组件,不如自己手写一个吧!

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
37.组件实战:Upload拖拽上传
Next
39.React组件库都是怎么构建的