手写实现 rc-field-form

1,032 阅读4分钟

我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!

写在前面

  • 前面几篇我们已经仿写实现了 rc-field-form 的大部分功能了
  • 在之前的实现中,我们在 Form 组件之外实现了一个数据仓库,并且通过自定义 hook useForm 将数据仓库实例的控制权限对象返回出去,这样我们需要操作数据仓库时,只用调用 useForm 就能拿到数据仓库的控制权限对象了
  • 然后我们通过 React 提供的 Context API 将数据仓库的控制权限,从 Form 组件顶层跨层级传递给它的子孙组件 Field 组件进行消费使用
  • 大家有没有发现,这种方案只能在 Form 组件内部及其子孙组件中才能操作数据仓库,那我们如何在 Form 组件外部操作数据仓库呢,例如:如何在使用 Form 组件的父组件挂载后,给 Form 组件的某个 Field 设置一个默认值?
  • 今天来实现一下这个需求

在函数组件中使用 Form 组件

  • 我们先来看下代码
export const FuncMyRCFormPage = () => {
  const [form] = Form.useForm();

  const onFinish = (val) => console.log("onFinish", val);

  const onFinishFailed = (val) => console.log("onFinishFailed", val);

  useEffect(() => {
    form.setFieldsValue({ user: "hook form default" });
  }, []);

  return (
    <div style={{ width: 500, margin: "auto" }}>
      <h2>func-my-rc-field-formPage</h2>
      <Form form={form} onFinish={onFinish} onFinishFailed={onFinishFailed}>
        <Field name="user" label="账号" rules={userRules}>
          <Input placeholder="请输入账号" />
        </Field>

        <Field name="pwd" label="密码" rules={pwdRules}>
          <Input placeholder="请输入密码" />
        </Field>

        <button>Submit</button>
      </Form>
    </div>
  );
};
  • 既然是函数组件,那么理所当然的我们可以使用 useForm 自定义 hook 来获取数据仓库的权限对象
  • 那么这样就会出现一个问题,怎么保证 FuncMyRCFormPage 这个组件使用的数据仓库权限对象,就是我们 Form 组件使用数据仓库权限对象呢?
  • 我们可以改造一下 useForm 的实现,代码如下
export function useForm(formStoreApi) {
  const formRef = useRef();

  if (!formRef.current) {
    if (formStoreApi) {
      formRef.current = formStoreApi;
    } else {
      const formStore = new FormStore();
      formRef.current = formStore.getForm();
    }
  }

  return [formRef.current];
}
  • useForm 可以接收一个数据仓库的操作权限对象 formStoreApi
    • 如果 formRef 对象已经保存了数据仓库操作权限对象,那就可以直接返回它
    • 否则,进行数据仓库操作权限对象的初始化操作
      • 如果有外部传入的权限对象,就用外部传入的
      • 如果外部没有传入就实例化一个新的数据仓库权限对象
  • 这样 FuncMyRCFormPage 如果使用 useForm 初始化了一个数据仓库的操作权限对象,就可以直接通过 props 传给 Form 组件
  • 同样的 Form 组件也需要做一些改写
export default function Form(
  { children, form, onFinishFailed, onFinish }
) {
  const [formStore] = useForm(form);

  // 注册 Form 上的回掉函数,数据校验后,提交数据时调用
  formStore.setCallbacks({
    onFinish,
    onFinishFailed,
  });

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        formStore.submit();
      }}
    >
      <FormContext.Provider value={formStore}>{children}</FormContext.Provider>
    </form>
  );
}
  • 只需要将接收的 props 中的数据仓库操作权限对象,也就是上面的 form,传给 useForm 即可

在类组件中使用 Form 组件

  • 上面函数组件可以使用自定义 hook,那么类组件该怎么拿到数据仓库的操作权限对象呢
  • 先让我们看看类组件的使用
export class ClassMyRCFormPage extends Component {
  formRef = React.createRef();

  componentDidMount() {
    this.formRef.current.setFieldsValue({ user: "class form default" });
  }

  onFinish = (val) => console.log("onFinish", val);

  onFinishFailed = (val) => console.log("onFinishFailed", val);

  render() {
    return (
      <div style={{ width: 500, margin: "auto" }}>
        <h2>class-my-rc-field-formPage</h2>
        <Form
          ref={this.formRef}
          onFinish={this.onFinish}
          onFinishFailed={this.onFinishFailed}
        >
          <Field name="user" label="账号" rules={userRules}>
            <Input placeholder="请输入账号" />
          </Field>

          <Field name="pwd" label="密码" rules={pwdRules}>
            <Input placeholder="请输入密码" />
          </Field>

          <button>Submit</button>
        </Form>
      </div>
    );
  }
}
  • 类组件确实无法使用自定义 hook,但是我们依然可以在 ClassMyRCFormPage 组件中使用 ref 去拿到其子组件 Form 中初始化的数据仓库操作权限对象
  • 但是我们的 Form 组件是函数组件,无法直接接收 ref,那该怎么办呢?
  • 好在 react 官网文档已经说了解决方案,下面是具体的解决方案代码
import React from "react";
import _Form from "./Form";
import { useForm } from "./useForm";

const Form = React.forwardRef(_Form);
Form.useForm = useForm;

export { Form };
export * from "./Input";
export * from "./Field";
  • 在组件导出时,使用 React.forwardRef 进行一次 ref 的转发,这样我们的 Form 组件就能接收到 ref 了
  • 然后再对我们的 Form 组件做一些修改
export default function Form(
  { children, form, onFinishFailed, onFinish },
  ref // 使用 forwardRef,将 Form 接收的 ref 进行转发
) {
  const [formStore] = useForm(form);

  // 通过 useImperativeHandle hook 将 formStore 暴露给父组件
  React.useImperativeHandle(ref, () => formStore);

  // 注册 Form 上的回掉函数,数据校验后,提交数据时调用
  formStore.setCallbacks({
    onFinish,
    onFinishFailed,
  });

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        formStore.submit();
      }}
    >
      <FormContext.Provider value={formStore}>{children}</FormContext.Provider>
    </form>
  );
}
  • 可以看到上面的代码中,使用了一个不咋常用的 hook api: useImperativeHandle
  • 它通常和 React.forwardRef 一起搭配使用,可以让我们在使用 ref 时自定义想要暴露给父组件的实例属性
  • 至此,我们仿写 rc-field-form 的所有需求就全部完成了

最后

  • 今天的分享就到这里了,欢迎大家在评论区里面进行讨论 👏。
  • 如果觉得文章写的不错的话,希望大家不要吝惜点赞,大家的鼓励是我分享的最大动力 🥰