玩转react表单

198 阅读2分钟

表单组件Form的基本目标

  1. 字段的统一收集和校验
  2. 适配各种组件的接入

高阶组件的form实现

在没有hooks的那个时期,react表单基本以高阶组件的形式实现。选用高阶组件主要还是为了将表单信息的注入到class组件的props中。

高阶函数嵌套高阶组件,传递配置参数后,再传入包裹的组件

Form.create = (options = {}) => {
   // options todo
   return (WrappedComponent) => {
       return class extends React.Component {
        ...
        addtionaProps = () => {
          return {
            getFieldDecorator: this.getFieldDecorator,
            setFieldsValue: this.fieldStore.setFieldsValue,
            getFieldValue: this.fieldStore.getFieldValue,
            validateFields: this.validateFieldsWraped,
            resetFields: this.fieldStore.resetFields,
            store: this.fieldStore.store,
            isSubmitting: this.submitting(),
          }
        }
                     
       render() {
         return {
            <WrappedComponent 
                {...this.props}
                {...this.addtionalProps()}
            />
        }
      }
    }
  }
}

表单项收集,也是通过高阶组件代理,代理表单触发方法,通过forceUpdate触发更新,此外fieldStore对表单的字段初始化、更新、状态做统一的管理控制

getFieldDecorator = ({
    name,
    rules = [], 
    initialValue,
    trigger = 'onChange',
    valueFormat,
    valueKey = 'value'
}) => {
    // todo... 表单仓库初始化, 注册字段和校验
    return (ComponentF) => {
        const triggerAction = async (e) => {
            ComponentF.props?.onChange(e)
            let actionValue = e
            if (valueFormat) actionValue = await valueFormat(e)
            this.fieldStore.dispatchStore('name', actionValue)
            this.forceUpdate()
        }
        ...
        const propsNew = {
            ...ComponentF.props,
        }
        propsNew[value] = this.fieldStore.getFieldValue(name)
        propsNew[trigger] = triggerAction
        
        return React.cloneElement(ComponentF, propsNew, ComponentF.props.children)
    }
}

高阶组件的更新过程

  1. 表单的trigger直接更改表单仓库字段值
  2. 触发最外层高阶函数更新,获取最新表单仓库
  3. 表单项通过高阶组件同步最新的数据

缺陷:高阶函数需要更多的执行成本,每次都需要创建新的表单项

优点:props的注入,每次更新fieldsValue等API获取的数据和视图是完全同步的

hooks的form实现

未命名文件.png 关键的两个点在useFormcontext共享, useForm通过useRef保存表单仓库的引用

function useForm(
  form?: IFormInstanceAPI,
  defaultFormValue = {},
): IFormInstanceAPI {
  const formRef: { current: IFormInstanceAPI | null } = useRef(null)
  const [, forceUpdate] = useState()

  useEffect(
    function () {
      if (formRef.current) {
        formRef.current.setFields(defaultFormValue)
      }
    },
    [defaultFormValue],
  )

  if (!formRef.current) {
    if (form) {
      formRef.current = form
    } else {
      const formStoreCurrent = new FormStore(forceUpdate, defaultFormValue)
      formRef.current = formStoreCurrent.getForm()
    }
  }

  return formRef.current
}

context通过Provider和useContext实现FormItem共享formStore

function Form(
  props: FormProps,
  ref: React.ForwardedRef<IFormInstanceAPI>,
): JSX.Element {

  const formInstance = useForm(form, initialValues)
  // todo...注册表单原生提交方法,暴露组件实例

  return (
    <form ...>
        <FormContext.Provider value={formInstance}>
          {children}
        </FormContext.Provider>
    </form>
  )
}

function FormItem(props: FormItemProps) {
  const formInstance = useContext<IFormInstanceAPI>(FormContext)
  // ...todo
}

优势:每次的更新成本控制在formItem内,减少了更新成本

缺陷:每次更新fieldsValue等API获取的数据和视图不一定同步,需要在trigger方法里面额外设置state或forceUpdate来获取到最新的数据