2021-05-05 React Hooks+TS实现表单rc-field-form

1,221 阅读5分钟

Antd4的Form表单的使用想必大家不会陌生,随着使用的越多,其背后的原理实现也是我们前端开发者想要了解的,接下来简单实现下表单的源码,使用React Hook + Typescript

Form表单无非做了一下几件事:

  • 数据收集
  • 数据传递
  • 数据响应式
  • 表单校验
  • 表单提交
  • 表单重置

数据收集

Form表单里的input、radio这些数据项需要做成受控组件,传统的做法是放到Form父组件中的state中,其中antd3就是这么做的,但是这种方式有一个缺点就是只要Form中有一个数据项发生了变化,那都要执行Form的setState,这就意味着整个Form表单都要更新,这不合理。

于是乎,类似于redux这种数据仓库的做法就收到了青睐,定义一个单独的数据仓库useForm(自定义Hook)在数据仓库中设置get和set方法暴露出来即可

定义了store对象来存储Field组件中input等数据项形式如[name]:value

定义组件实例数组fieldEntities存取Field组件实例,包括{props, onStoreChange}

// useForm.ts

/**
 * 数据仓库-存储form
 */

class FormStore {
  // 数据仓库
  private store: Store = {};
  ...

  // get
  private getFieldsValue = (): Store => {
    return this.store;
  };

  private getFieldValue = (name: string): any => {
    return this.store[name];
  };

  // set
  private setFieldsValue = (newStore: Store): void => {
    // 'name': 'value'
    // 第一步:更新数据仓库
    this.store = Object.assign({}, this.store, newStore);

    // 第二步:更新组件-forceUpdate
    this.fieldEntities.forEach(field => {
      // 更新对应name上的field,而不是每次都全部更新
      Object.keys(newStore).forEach(k => {
        if (k === field.props.name) {
          field.onStoreChange();
        }
      });
    });
  };

  ...

  // 提交
  private submit = () => {
     // 校验成功onFinish
     // 校验失败onFinishFailed
  };

  // 给用户暴露的API
  public getForm = (): FormInstance => ({
    getFieldValue: this.getFieldValue,
    getFieldsValue: this.getFieldsValue,
    setFieldsValue: this.setFieldsValue,
    submit: this.submit
  });
}

...

数据仓库创建好后,需要存储实例,需要注意的是组件会发生更新,要确保组件更新和初次渲染的时候使用的是同一个数据仓库实例,则需要使用useRef来保证在组件整个生命周期间保持不变

// useForm.ts

...

function useForm<Values = any>(
  form?: FormInstance<Values>
): [FormInstance<Values>] {
  // 保证始终用到同一个对象
  const formRef = React.useRef<FormInstance>();
  if (!formRef.current) {
      const formStore: FormStore = new FormStore();
      formRef.current = formStore.getForm();
  }

  return [formRef.current];
}

数据传递

数据仓库创建好后,接下来就是各个组件对数据仓库的访问。考虑到Form组件、input、radio组件等都要访问数据仓库,并且它们有个共同的特点,都是Form的子组件,只是不确定是Form的第几代子孙组件,这个时候使用props显然不合适,所以需要React提供的context方法跨层级传递对象,分为三步走:

1、创建context对象

// FieldContext.ts

import React from "react";

const FieldContext = React.createContext<FormInstance>();

export default FieldContext;

2、使用Provider传递value

// Form.tsx
...
import FieldContext from "./Context";
import useForm from "./useForm";

export default function Form({children}){
    
    const [formInstance] = useForm()
    <form onSubmit={onSubmit}>
      <FieldContext.Provider value={formInstance}>
        {children}
      </FieldContext.Provider>
    </form>
}

3、子组件消费value,函数组件中使用useContext(context) Hook方法

// Field.tsx

// 获取context对象
const fieldContext = useContext(FieldContext);
// 额外的属性-input
function getControlled() {
const { getFieldValue, setFieldsValue } = fieldContext;

return {
  value: getFieldValue(name), // get
  onChange: (e: any) => {
    // set
    const newValue = e.target.value;
    setFieldsValue({ [name]: newValue });
  }
};
}

// 克隆组件,给其添加一些属性返回,使Field=>Input变成受控组件
const returnChildNode = React.cloneElement(
children as React.ReactElement,
getControlled()
);

return (
<div style={{ position: "relative" }}>
  <div style={{ display: "flex", alignItems: "center" }}>
    <label style={{ flex: "none", width: 50 }}>{label || name} </label>
    {returnChildNode}
  </div>
</div>
);

数据响应式

用Field包裹的input、radio等数据项缺少数据响应式,即value和onChange事件,另外store中的数据发生改变,Field组件也应该随之更新,而React中更新组件有四种方式:ReactDOM.render、forceUpdate、setState或者父组件更新,这里应该用forceUpdate

那么现在其实要做的就是加上注册组件更新,监听this.store,一旦this.store中的某个值改变,就更新对应的组件。

首先在FormStore中加上存储Form中子组件实例的方法

// useForm.ts

class FormStore {
  // 数据仓库
  private store: Store = {};
  // 组件实例数组
  private fieldEntities: FieldEntity[] = [];
  ...
  
  // 存取组件实例
  private setFieldEntities = (field: FieldEntity) => {
    this.fieldEntities.push(field);
    return () => {
      // 取消注册
      this.fieldEntities = this.fieldEntities.filter(f => f !== field);
      // 删除数据仓库里的数据
      delete this.store[field.props.name];
    };
  };

  // get
  private getFieldsValue = (): Store => {
    return this.store;
  };

  private getFieldValue = (name: string): any => {
    return this.store[name];
  };

  // set
  private setFieldsValue = (newStore: Store): void => {
    // 'name': 'value'
    // 第一步:更新数据仓库
    this.store = Object.assign({}, this.store, newStore);

    // 第二步:更新组件-forceUpdate
    this.fieldEntities.forEach(field => {
      // 更新对应name上的field,而不是每次都全部更新
      Object.keys(newStore).forEach(k => {
        if (k === field.props.name) {
          field.onStoreChange();
        }
      });
    });
  };

  ....
}

接下来在Field组件中执行注册和取消注册

const Field: React.FC<FieldProps> = (props: FieldProps) => {
  const { children, label, name } = props;
  // 获取context对象
  const fieldContext = useContext(FieldContext);
  const [, forceUpdate] = useReducer(x => x + 1, 0);

  // 使用useEffect可能会使订阅延迟
  useLayoutEffect(() => {
    // 函数组件没有实例对象this,把props和onStoreChange当做对象传过去
    const unRegister: any = fieldContext.setFieldEntities({
      props,
      onStoreChange
    });

    // 组件销毁取消订阅
    return () => {
      unRegister();
    };
  }, [props, fieldContext]);

  // 强制更新组件
  function onStoreChange() {
    forceUpdate();
  }

  ....
};

表单检验

提交前,首先要做表单校验。校验通过,则执行onFinish,失败则执行onFinishFailed,表单校验的依据是Field的rules,这里做的简单的校验,只要不是null、undefined或者空字符串,就当校验通过,否则的话,校验不通过,并且err数组push错误信息

// useForm.ts

// 校验
  private validate = () => {
    let err: object[] = [];
    // 实现基础功能,如输入信息就通过
    this.fieldEntities.forEach(field => {
      const { name, rules } = field.props;
      let rule = rules && rules[0];
      let value = this.getFieldValue(name);
      if (
        rule &&
        rule.required &&
        (value == undefined || value.replace(/\s*/, "") === "")
      ) {
        err.push({ name, err: rule.message });
      }
    });

    return err;
  };

表单提交

完成表单校验后,就是表单提交方法,即onFinish和onFinishFailed方法,由于这两个是Form组件的props参数,所以可以在FormStore中定义this.callbacks存储

// useForm.ts

class FormStore{
  // 保存成功和失败回调函数
  private callbacks: Callbacks = {};
  ...
  // 存取回调函数-成功或失败
  private setCallbacks = (callbacks: Callbacks) => {
    this.callbacks = callbacks;
  };
}

接下李在Form中执行setCallbacks即可

// Form.tsx

// 存取回调函数-成功和失败
formInstance.setCallbacks({ onFinish, onFinishFailed });

表单重置

给表单设置初始值,即Field的属性initialValue,要保证我们使用的数据仓库在组件的任何生命周期都是同一个,即初始化一次,后续在这个基础上更新,给Form的props加个参数form

// Form.tsx

export default function Form({..., form}){
    const [formInstance] = useForm(form)
}

修改后的useForm如下:

// useForm.ts

...
function useForm<Values = any>(
  form?: FormInstance<Values>
): [FormInstance<Values>] {
  // 保证始终用到同一个对象
  const formRef = React.useRef<FormInstance>();
  if (!formRef.current) {
    if (form) {
      // 默认值
      formRef.current = form;
    } else {
      const formStore: FormStore = new FormStore();
      formRef.current = formStore.getForm();
    }
  }

  return [formRef.current];
}

设置初始值和重置方法在FormStore中

// useForm.ts

class FormStore{
...
// 初始值
private initialValues: Store = {};

// 存取初始值
private setInitialValues = (initialStore: Store): void => {
    this.initialValues = Object.assign({}, this.initialValues, initialStore);
};
  
// 重置
private resetFields = (): void => {
    this.setFieldsValue(this.initialValues);
};

}

至此,一个简单的Form表单就差不多了,代码地址github地址