likes
comments
collection
share

react-hook-form 原理及实践通过源码阅读,带大家梳理 react-hook-form 的原理,以及分享一些

作者站长头像
站长
· 阅读数 45

前言

最近在做一个批量编辑商品的需求,需要结合 table 和 form 使用。我们使用了 react-hook-form 作为表单方案。而在实际开发过程中,table 的列数量达到 20+,每个列都对应一个表单控件,这对性能有极大要求,使用不当的话会造成页面会卡顿,影响用户体验。因此,有必要对 react-hook-form 做一个深入的学习了解。下面我将 react-hook-form 的核心原理以及实际开发过程中踩到的坑分享出来,希望对大家有所帮助。

官网链接: react-hook-form.com/get-started

核心原理

useForm

useForm 是使用 react-hook-form 的入口函数,我们简单看一下这个函数做了什么

简化代码如下:

export function useForm<
  TFieldValues extends FieldValues = FieldValues,
  TContext = any,
  TTransformedValues extends FieldValues | undefined = undefined,
>(
  props: UseFormProps<TFieldValues, TContext> = {},
): UseFormReturn<TFieldValues, TContext, TTransformedValues> {
  const _formControl = React.useRef<
    UseFormReturn<TFieldValues, TContext, TTransformedValues> | undefined
  >();
  const _values = React.useRef<typeof props.values>();
  // formState 由 react.useState 来控制
  const [formState, updateFormState] = React.useState<FormState<TFieldValues>>({
    isDirty: false,
    isValidating: false,
    isLoading: isFunction(props.defaultValues),
    isSubmitted: false,
    isSubmitting: false,
    isSubmitSuccessful: false,
    isValid: false,
    submitCount: 0,
    dirtyFields: {},
    touchedFields: {},
    validatingFields: {},
    errors: props.errors || {},
    disabled: props.disabled || false,
    defaultValues: isFunction(props.defaultValues)
      ? undefined
      : props.defaultValues,
  });
  
  ...
  // 订阅 formState 的变化,当 formState 更改时,调用 updateFormState 触发 rerender
  useSubscribe({
    subject: control._subjects.state,
    next: (
      value: Partial<FormState<TFieldValues>> & { name?: InternalFieldName },
    ) => {
      if (
        shouldRenderFormState(
          value,
          control._proxyFormState,
          control._updateFormState,
          true,
        )
      ) {
        updateFormState({ ...control._formState });
      }
    },
  });

  // 初始化数据
  React.useEffect(() => {
    if (props.values && !deepEqual(props.values, _values.current)) {
      control._reset(props.values, control._options.resetOptions);
      _values.current = props.values;
      updateFormState((state) => ({ ...state }));
    } else {
      control._resetDefaultValues();
    }
  }, [props.values, control]);

   // 初始化错误
  React.useEffect(() => {
    if (props.errors) {
      control._setErrors(props.errors);
    }
  }, [props.errors, control]);

  React.useEffect(() => {
    props.shouldUnregister &&
      control._subjects.values.next({
        values: control._getWatch(),
      });
  }, [props.shouldUnregister, control]);

  // 可以看到 formState 做了一层 proxy 代理,作用是什么呢?我们在后面会讲到
  _formControl.current.formState = getProxyFormState(formState, control);

  return _formControl.current;
}

订阅发布系统

React-hook-form 自身实现了一套订阅发布系统。在 useForm 的源码中,我们可以看到其内部使用了 useSubscribe,其订阅了 control._subjects.state。其实实现也很简单,下面我们来一起看下:


type Props<T> = {
  disabled?: boolean;
  subject: Subject<T>;
  next: (value: T) => void;
};
  
export function useSubscribe<T>(props: Props<T>) {
  const _props = React.useRef(props);
  _props.current = props;

  React.useEffect(() => {
    const subscription =
      !props.disabled &&
      _props.current.subject &&
      _props.current.subject.subscribe({
        next: _props.current.next,
      });

    return () => {
      subscription && subscription.unsubscribe();
    };
  }, [props.disabled]);
}

// control._subject 初始化如下:
const _subjects: Subjects<TFieldValues> = {
    values: createSubject(),
    array: createSubject(),
    state: createSubject(),
};

// useSubscribe 的使用方式如下:
// 对 state 进行订阅
useSubscribe({subject: control._subject.state, next(){
    // do something
} })

在 react-hook-form 中,有三个可订阅的对象,分别是 values、array、state.

可以这么简单理解:

  • Values 管理的是非数组类型值的状态变更

  • Array 管理的是数组类型值的状态变更

  • State 管理的是表单状态的变更,例如 errors

createSubject 实现:

import { Noop } from '../types';

export type Observer<T> = {
  next: (value: T) => void;
};

export type Subscription = {
  unsubscribe: Noop;
};

export type Subject<T> = {
  readonly observers: Observer<T>[];
  subscribe: (value: Observer<T>) => Subscription;
  unsubscribe: Noop;
} & Observer<T>;

export default <T>(): Subject<T> => {
  let _observers: Observer<T>[] = [];

  // 发布,触发所有执行过订阅的回调
  const next = (value: T) => {
    for (const observer of _observers) {
      observer.next && observer.next(value);
    }
  };

  // 订阅,将回调函数收集到 _observers 中
  const subscribe = (observer: Observer<T>): Subscription => {
    _observers.push(observer);
    return {
      unsubscribe: () => {
        _observers = _observers.filter((o) => o !== observer);
      },
    };
  };

  const unsubscribe = () => {
    _observers = [];
  };

  return {
    get observers() {
      return _observers;
    },
    next,
    subscribe,
    unsubscribe,
  };
};

onChange

onChange 是很重要的 API,它关乎到一个表单控件是否能正确渲染对应的值,下面我们来看下具体实现:

onChange: React.useCallback(
    (event) =>
      _registerProps.current.onChange({
        target: {
          value: getEventValue(event),
          name: name as InternalFieldName,
        },
        type: EVENTS.CHANGE,
      }),
    [name],
),

_registerProps.current.onChange 实现如下:

  const onChange: ChangeHandler = async (event) => {
    _state.mount = true;
    const target = event.target;
    let name = target.name as string;
    let isFieldValueUpdated = true;
    const field: Field = get(_fields, name);
    const getCurrentFieldValue = () =>
      target.type ? getFieldValue(field._f) : getEventValue(event);
    const _updateIsFieldValueUpdated = (fieldValue: any): void => {
      isFieldValueUpdated =
        Number.isNaN(fieldValue) ||
        deepEqual(fieldValue, get(_formValues, name, fieldValue));
    };

    if (field) {
      let error;
      let isValid;
      const fieldValue = getCurrentFieldValue();
      const isBlurEvent =
        event.type === EVENTS.BLUR || event.type === EVENTS.FOCUS_OUT;
        
      // 判断是否可以跳过验证
      const shouldSkipValidation =
        (!hasValidation(field._f) &&
          !_options.resolver &&
          !get(_formState.errors, name) &&
          !field._f.deps) ||
        skipValidation(
          isBlurEvent,
          get(_formState.touchedFields, name),
          _formState.isSubmitted,
          validationModeAfterSubmit,
          validationModeBeforeSubmit,
        );
      const watched = isWatched(name, _names, isBlurEvent);
        
      // 更新 _formValues 的值,这个 _formValues 可以通过 control._formValues 访问
      // 注意,这个 set 方法只是简单的更改 _formValues 对象的属性值,不会触发 rerender
      set(_formValues, name, fieldValue);

      if (isBlurEvent) {
        field._f.onBlur && field._f.onBlur(event);
        delayErrorCallback && delayErrorCallback(0);
      } else if (field._f.onChange) {
        field._f.onChange(event);
      }
      
      const fieldState = updateTouchAndDirty(
        name,
        fieldValue,
        isBlurEvent,
        false,
      );

      const shouldRender = !isEmptyObject(fieldState) || watched;

      // 触发 _subjects.values 的订阅执行
      !isBlurEvent &&
        _subjects.values.next({
          name,
          type: event.type,
          values: { ..._formValues },
        });

      if (shouldSkipValidation) {
        if (_proxyFormState.isValid) {
          if (props.mode === 'onBlur') {
            if (isBlurEvent) {
              _updateValid();
            }
          } else {
            _updateValid();
          }
        }

        return (
          shouldRender &&
          _subjects.state.next({ name, ...(watched ? {} : fieldState) })
        );
      }
        
      // 如果有 watch,则触发这个 form 的 rerender
      !isBlurEvent && watched && _subjects.state.next({ ..._formState });

      // 传入了 resolver 参数,使用自定义的校验库,如 Yup, Zod, Joi, Vest, Ajv 
      if (_options.resolver) {
        const { errors } = await _executeSchema([name]);

        _updateIsFieldValueUpdated(fieldValue);

        if (isFieldValueUpdated) {
          const previousErrorLookupResult = schemaErrorLookup(
            _formState.errors,
            _fields,
            name,
          );
          const errorLookupResult = schemaErrorLookup(
            errors,
            _fields,
            previousErrorLookupResult.name || name,
          );

          error = errorLookupResult.error;
          name = errorLookupResult.name;

          isValid = isEmptyObject(errors);
        }
      } else {
        _updateIsValidating([name], true);
        // 注意,这里是异步校验,错误暂时还未能更新
        // 如果你在 validate 或 change 中依赖了后端接口进行校验,且接口参数依赖了 formState.errors
        // 此时的 formState.errors 是校验前的 errors
        error = (
          await validateField(
            field,
            _formValues,
            shouldDisplayAllAssociatedErrors,
            _options.shouldUseNativeValidation,
          )
        )[name];
        _updateIsValidating([name]);

        _updateIsFieldValueUpdated(fieldValue);

        if (isFieldValueUpdated) {
          if (error) {
            isValid = false;
          } else if (_proxyFormState.isValid) {
            isValid = await executeBuiltInValidation(_fields, true);
          }
        }
      }

      if (isFieldValueUpdated) {
        field._f.deps &&
          trigger(
            field._f.deps as
              | FieldPath<TFieldValues>
              | FieldPath<TFieldValues>[],
          );
        // 更新错误,触发 rerender
        shouldRenderByError(name, isValid, error, fieldState);
      }
    }
  };

setValue

当我们要去处理表单联动逻辑时,这个 api 很有用,看下是怎么实现的

  const setValue: UseFormSetValue<TFieldValues> = (
    name,
    value,
    options = {},
  ) => {
    const field = get(_fields, name);
    const isFieldArray = _names.array.has(name);
    const cloneValue = cloneObject(value);
    
    // 更新 control._formValues,不会触发 rerender
    set(_formValues, name, cloneValue);

    // 是否是数组
    if (isFieldArray) {
      // 触发 array 的订阅执行
      _subjects.array.next({
        name,
        values: { ..._formValues },
      });

      if (
        (_proxyFormState.isDirty || _proxyFormState.dirtyFields) &&
        options.shouldDirty
      ) {
        // 触发 state 的订阅执行
        _subjects.state.next({
          name,
          dirtyFields: getDirtyFields(_defaultValues, _formValues),
          isDirty: _getDirty(name, cloneValue),
        });
      }
    } else {
      // setValues 和 setFieldValue 的实现就不展开了,
      // 简化流程就是
      // 1. 触发 values 的订阅执行
      // 2. 如果 setValue 的入参 options.shouldValidate = true,则会触发当前控件的校验
      field && !field._f && !isNullOrUndefined(cloneValue)
        ? setValues(name, cloneValue, options)
        : setFieldValue(name, cloneValue, options);
    }
    // 如果被 watch 了,触发 state 的订阅执行
    isWatched(name, _names) && _subjects.state.next({ ..._formState });
    // 触发 values 的订阅执行
    _subjects.values.next({
      name: _state.mount ? name : undefined,
      values: { ..._formValues },
    });
  };

setError

setError 的实现很简单,大家看下就行

  const setError: UseFormSetError<TFieldValues> = (name, error, options) => {
    const ref = (get(_fields, name, { _f: {} })._f || {}).ref;
    const currentError = get(_formState.errors, name) || {};

    const { ref: currentRef, message, type, ...restOfErrorTree } = currentError;

    set(_formState.errors, name, {
      ...restOfErrorTree,
      ...error,
      ref,
    });

    _subjects.state.next({
      name,
      errors: _formState.errors,
      isValid: false,
    });

    options && options.shouldFocus && ref && ref.focus && ref.focus();
  };

useFieldArray

export function useFieldArray<
  TFieldValues extends FieldValues = FieldValues,
  TFieldArrayName extends
    FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>,
  TKeyName extends string = 'id',
>(
  props: UseFieldArrayProps<TFieldValues, TFieldArrayName, TKeyName>,
): UseFieldArrayReturn<TFieldValues, TFieldArrayName, TKeyName> {
  const methods = useFormContext();
  const {
    control = methods.control,
    name,
    keyName = 'id',
    shouldUnregister,
  } = props;
  // useFieldArray 内部维护了一个 state
  const [fields, setFields] = React.useState(control._getFieldArray(name));
  const ids = React.useRef<string[]>(
    control._getFieldArray(name).map(generateId),
  );
  const _fieldIds = React.useRef(fields);
  const _name = React.useRef(name);
  const _actioned = React.useRef(false);

  _name.current = name;
  _fieldIds.current = fields;
  control._names.array.add(name);

  props.rules &&
    (control as Control<TFieldValues>).register(
      name as FieldPath<TFieldValues>,
      props.rules as RegisterOptions<TFieldValues>,
    );

  // 订阅 control._subjects.array
  useSubscribe({
    next: ({
      values,
      name: fieldArrayName,
    }: {
      values?: FieldValues;
      name?: InternalFieldName;
    }) => {
      if (fieldArrayName === _name.current || !fieldArrayName) {
        const fieldValues = get(values, _name.current);
        if (Array.isArray(fieldValues)) {
          setFields(fieldValues);
          ids.current = fieldValues.map(generateId);
        }
      }
    },
    subject: control._subjects.array,
  });

  const updateValues = React.useCallback(
    <
      T extends Partial<
        FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>
      >[],
    >(
      updatedFieldArrayValues: T,
    ) => {
      _actioned.current = true;
      control._updateFieldArray(name, updatedFieldArrayValues);
    },
    [control, name],
  );

  const append = (
    value:
      | Partial<FieldArray<TFieldValues, TFieldArrayName>>
      | Partial<FieldArray<TFieldValues, TFieldArrayName>>[],
    options?: FieldArrayMethodProps,
  ) => {
    const appendValue = convertToArrayPayload(cloneObject(value));
    const updatedFieldArrayValues = appendAt(
      control._getFieldArray(name),
      appendValue,
    );
    control._names.focus = getFocusFieldName(
      name,
      updatedFieldArrayValues.length - 1,
      options,
    );
    ids.current = appendAt(ids.current, appendValue.map(generateId));
    updateValues(updatedFieldArrayValues);
    // rerender
    setFields(updatedFieldArrayValues);
    // _updateFieldArray 的简化流程是
    // 根据 dirty 的值判断是否调用 _subjects.state.next
    // 否则调用 set 更新
    control._updateFieldArray(name, updatedFieldArrayValues, appendAt, {
      argA: fillEmptyArray(value),
    });
  };
    ...

  return {
    swap: React.useCallback(swap, [updateValues, name, control]),
    move: React.useCallback(move, [updateValues, name, control]),
    prepend: React.useCallback(prepend, [updateValues, name, control]),
    append: React.useCallback(append, [updateValues, name, control]),
    remove: React.useCallback(remove, [updateValues, name, control]),
    insert: React.useCallback(insert, [updateValues, name, control]),
    update: React.useCallback(update, [updateValues, name, control]),
    replace: React.useCallback(replace, [updateValues, name, control]),
    fields: React.useMemo(
      () =>
        fields.map((field, index) => ({
          ...field,
          [keyName]: ids.current[index] || generateId(),
        })) as FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>[],
      [fields, keyName],
    ),
  };
}

小结

这里再简单画个图来梳理一下主要流程:

react-hook-form 原理及实践通过源码阅读,带大家梳理 react-hook-form 的原理,以及分享一些

这里的 useWatch 和 useFormState 的原理下文再做解释,这里可以简单地理解成对 value 和 state 对订阅,当 value 或 state 变动时,就会触发当前组件的 rerender。

实践/踩坑

下面我列出一些问题,以及之前所踩到的坑,加强一下大家对 react-hook-form 的理解

Watch vs useWatch ?

当我们查阅官方文档时,会发现值的订阅有两种方式,分别是使用 watch 和 useWatch ,那么两者有何差异呢?下面我们以一个具体的例子开始说明

例子

import React, { useEffect } from 'react';
import {
  Controller,
  FormProvider,
  useForm,
  useFormContext,
  useWatch,
} from 'react-hook-form';

// 使用 watch
function InputWatch() {
  const { control, setError, watch } = useFormContext();
  const input1 = watch('input1');
  console.log('<<<<<<< InputWatch rerender >>>>>');

  return (
    <Controller
      name="InputWatch"
      render={({ field }) => <input {...field} />}
      control={control}
    />
  );
}

// 使用 useWatch
function InputUseWatch() {
  const { control, setError } = useFormContext();
  const input1 = useWatch({ name: 'input1' });
  console.log('<<<<<<< InputUseWatch rerender >>>>>');

  return (
    <Controller
      name="InputUseWatch"
      render={({ field }) => <input {...field} />}
      control={control}
    />
  );
}

export const TestWatch: React.FC = () => {
  const form = useForm();

  console.log('<<<<<<< form rerender >>>>>');

  return (
    <FormProvider {...form}>
      <div>
        <Controller
          name="input1"
          render={({ field }) => <input {...field} />}
          control={form.control}
        />
        {/* 分别注释 InputWatch 和 InputUseWatch 查看日志打印 */}
        <InputWatch></InputWatch>
        {/* <InputUseWatch></InputUseWatch> */}
      </div>
    </FormProvider>
  );
};

我们可以看到,当使用 InputWatch 时, 改变 input1 的值 ,日志如下:

 <<<<<<< form rerender >>>>>
<<<<<<< InputWatch rerender >>>>>

而当使用 InputUseWatch 时,改变 input1 的值,日志如下:

<<<<<<< InputUseWatch rerender >>>>>

使用 watch('field') 时 , 当 'filed' 改变,会触发整个表单的 rerender,性能不佳;

使用 useWatch('field') 时,当 'filed' 改变时,只会触发使用 useWatch 的组件的 rerender,性能更好

那么两者为何有性能差异呢?下面我们来具体看下实现原理:

实现原理

**Watch **
  const watch: UseFormWatch<TFieldValues> = (
    name?:
      | FieldPath<TFieldValues>
      | ReadonlyArray<FieldPath<TFieldValues>>
      | WatchObserver<TFieldValues>,
    defaultValue?: DeepPartial<TFieldValues>,
  ) => _getWatch(
          name as InternalFieldName | InternalFieldName[],
          defaultValue,
          true,
    );
    
   const _getWatch: WatchInternal<TFieldValues> = (
    names,
    defaultValue,
    isGlobal,
  ) =>
    generateWatchOutput(
      names,
      _names,
      {
        ...(_state.mount
          ? _formValues
          : isUndefined(defaultValue)
            ? _defaultValues
            : isString(names)
              ? { [names]: defaultValue }
              : defaultValue),
      },
      // 这里 isGlobal 传入了 true
      isGlobal,
      defaultValue,
    );
    

// generateWatchOutput 实现
export default <T>(
  names: string | string[] | undefined,
  _names: Names,
  formValues?: FieldValues,
  isGlobal?: boolean,
  defaultValue?: DeepPartial<T> | unknown,
) => {
  if (isString(names)) {
  // 将使用 watch 组件的 name 记录下来
    isGlobal && _names.watch.add(names);
    return get(formValues, names, defaultValue);
  }
   ...
  return formValues;
};

当组件内部调用 setValue 去更新值时:

  const setValue: UseFormSetValue<TFieldValues> = (
    name,
    value,
    options = {},
  ) => {
      ...
      // 如果当前 name 已经被 watch 了,则更新表单,导致整个表单 rerender
      isWatched(name, _names) && _subjects.state.next({ ..._formState });
      ...
  } 

useWatch
export function useWatch<TFieldValues extends FieldValues>(
  props?: UseWatchProps<TFieldValues>,
) {
  const methods = useFormContext();
  const {
    control = methods.control,
    name,
    defaultValue,
    disabled,
    exact,
  } = props || {};
  const _name = React.useRef(name);

  _name.current = name;

 // 监听 control._subjects.values 的变化
  useSubscribe({
    disabled,
    subject: control._subjects.values,
    next: (formState: { name?: InternalFieldName; values?: FieldValues }) => {
      if (
        // useWatch 传入的 name 如果和订阅回调里传入的 name 相同时,
        // 则触发使用 useWatch 组件的 rerender
        shouldSubscribeByName(
          _name.current as InternalFieldName,
          formState.name,
          exact,
        )
      ) {
        updateValue(
          cloneObject(
            generateWatchOutput(
              _name.current as InternalFieldName | InternalFieldName[],
              control._names,
              formState.values || control._formValues,
              false,
              defaultValue,
            ),
          ),
        );
      }
    },
  });

  const [value, updateValue] = React.useState(
    control._getWatch(
      name as InternalFieldName,
      defaultValue as DeepPartialSkipArrayKey<TFieldValues>,
      // 这里没有传入 isGloabal 参数,则 isGloabal 为 false,此时的 watch 不会被记录到
    ),
  );

  React.useEffect(() => control._removeUnmounted());

  return value;
}

formState vs useFormState

官方文档有提到使用 useFormState 可以获得更好的性能,更适用于复杂表单。但在没有深究源码之前,给开发者的体感是,两者的用法和实际表现效果都差不多,但真的是这样吗?

举个例子:

场景一:在根组件里读取 formState.errors

import React, { useEffect } from 'react';
import {
  Controller,
  FormProvider,
  useForm,
  useFormContext,
} from 'react-hook-form';

function Input() {
  const { control, setError } = useFormContext();

  return (
    <Controller
      name="test"
      render={({ field: { onChange, value }, fieldState: { error } }) => (
        <>
          <input
            style={{ background: 'yellow', color: 'black' }}
            value={value}
            onChange={(e) => {
              onChange(e.target.value);
              setError('test', { message: e.target.value });
            }}
          />
          <div style={{ background: 'red' }}>{error?.message}</div>
        </>
      )}
      control={control}
    />
  );
}

export const TestFormState: React.FC = () => {
  const form = useForm();

  console.log('<<<<<<< form rerender >>>>>');
  // 尝试打开或关闭注释观察日志打印
  console.log(form.formState.errors, '<<<<<<< form.formState.errors >>>>>');

  return (
    <FormProvider {...form}>
      <Input></Input>
    </FormProvider>
  );
};

我们可以看到,没有注释日志时,通过改变 input 的值去 setError,日志如下:

<<<<<<< form rerender >>>>>
<<<<<<< form.formState.errors >>>>>

当注释调日志时,通过改变 input 的值去 setError,没有日志输入,证明根组件没有 rerender。

上面的现象表明,只要我们在根组件读取了 errors 的值,那么根组件就会 rerender。

场景二:在子组件里通过 useFormContext 读取 formState.errors

function Input() {
  const { control, setError, formState } = useFormContext();
  console.log(formState.errors, '<<<<<<< Input formState.errors >>>>>');

  return (
    <Controller
      name="test"
      render={({ field: { onChange, value }, fieldState: { error } }) => (
        <>
          <input
            style={{ background: 'yellow', color: 'black' }}
            value={value}
            onChange={(e) => {
              onChange(e.target.value);
              setError('test', { message: e.target.value });
            }}
          />
          <div style={{ background: 'red' }}>{error?.message}</div>
        </>
      )}
      control={control}
    />
  );
}

当改变 input 的值时,日志输出如下:

<<<<<<< form rerender >>>>>
<<<<<<< Input formState.errors >>>>>

场景三:在子组件里通过 useFormState 读取 formState.errors

function Input() {
  const { control, setError } = useFormContext();
  const formState = useFormState();
  console.log(formState.errors, '<<<<<<< Input formState.errors >>>>>');

  return (
    <Controller
      name="test"
      render={({ field: { onChange, value }, fieldState: { error } }) => (
        <>
          <input
            style={{ background: 'yellow', color: 'black' }}
            value={value}
            onChange={(e) => {
              onChange(e.target.value);
              setError('test', { message: e.target.value });
            }}
          />
          <div style={{ background: 'red' }}>{error?.message}</div>
        </>
      )}
      control={control}
    />
  );
}

当改变 input 的值时,只会触发子组件的 rerender ,不会影响根组件 ,日志输出如下:

<<<<<<< Input formState.errors >>>>>

综上得出的结论是:对于性能要求高的场景,无论是子组件还是父组件,都不推荐直接访问 FormState,而应通过 useFormState 访问。

原理解析

FormState 的代理机制
export function useForm<
  TFieldValues extends FieldValues = FieldValues,
  TContext = any,
  TTransformedValues extends FieldValues | undefined = undefined,
>(
  props: UseFormProps<TFieldValues, TContext> = {},
): UseFormReturn<TFieldValues, TContext, TTransformedValues> {
  const _formControl = React.useRef<
    UseFormReturn<TFieldValues, TContext, TTransformedValues> | undefined
  >();
  const _values = React.useRef<typeof props.values>();
  // formState 由 react.useState 来控制
  const [formState, updateFormState] = React.useState<FormState<TFieldValues>>({
    isDirty: false,
    isValidating: false,
    isLoading: isFunction(props.defaultValues),
    isSubmitted: false,
    isSubmitting: false,
    isSubmitSuccessful: false,
    isValid: false,
    submitCount: 0,
    dirtyFields: {},
    touchedFields: {},
    validatingFields: {},
    errors: props.errors || {},
    disabled: props.disabled || false,
    defaultValues: isFunction(props.defaultValues)
      ? undefined
      : props.defaultValues,
  });
  
  ...
  // 订阅 formState 的变化,当 formState 更改时,调用 updateFormState 触发 rerender
  useSubscribe({
    subject: control._subjects.state,
    next: (
      value: Partial<FormState<TFieldValues>> & { name?: InternalFieldName },
    ) => {
      if (
        shouldRenderFormState(
          value,
          control._proxyFormState,
          control._updateFormState,
          true,
        )
      ) {
        updateFormState({ ...control._formState });
      }
    },
  });

  // 可以看到 formState 做了一层 proxy 代理
  _formControl.current.formState = getProxyFormState(formState, control);

  return _formControl.current;
}

getProxyFormState 实现:

import { VALIDATION_MODE } from '../constants';
import { Control, FieldValues, FormState, ReadFormState } from '../types';

export default <TFieldValues extends FieldValues, TContext = any>(
  formState: FormState<TFieldValues>,
  control: Control<TFieldValues, TContext>,
  localProxyFormState?: ReadFormState,
  isRoot = true,
) => {
  const result = {
    defaultValues: control._defaultValues,
  } as typeof formState;

  for (const key in formState) {
    Object.defineProperty(result, key, {
      get: () => {
        const _key = key as keyof FormState<TFieldValues> & keyof ReadFormState;

        if (control._proxyFormState[_key] !== VALIDATION_MODE.all) {
          control._proxyFormState[_key] = !isRoot || VALIDATION_MODE.all;
        }
        localProxyFormState && (localProxyFormState[_key] = true);
        return formState[_key];
      },
    });
  }
  return result;
};

_proxyFormState 的结构如下:

const _proxyFormState: ReadFormState = {
    isDirty: false,
    dirtyFields: false,
    validatingFields: false,
    touchedFields: false,
    isValidating: false,
    isValid: false,
    errors: false,
};

可以看到,当我们读取了 formState 的某个属性时,_proxyFormState 对应的属性项的值就会设置成 true。

那么对于 _proxyFormState react-hook-form 又是如何处理的呢?

在 useForm 中有段逻辑:

  useSubscribe({
    subject: control._subjects.state,
    next: (
      value: Partial<FormState<TFieldValues>> & { name?: InternalFieldName },
    ) => {
      if (
        shouldRenderFormState(
          value,
          control._proxyFormState,
          control._updateFormState,
          true,
        )
      ) {
        updateFormState({ ...control._formState });
      }
    },
  });

这里会订阅 formState 的变化,当 formState 更改时,通过调用 shouldRenderFormState 判断是否需要调用 updateFormState 触发 rerender。

shouldRenderFormState 的实现如下:

import { VALIDATION_MODE } from '../constants';
import {
  Control,
  FieldValues,
  FormState,
  InternalFieldName,
  ReadFormState,
} from '../types';
import isEmptyObject from '../utils/isEmptyObject';

export default <T extends FieldValues, K extends ReadFormState>(
  formStateData: Partial<FormState<T>> & { name?: InternalFieldName },
  _proxyFormState: K,
  updateFormState: Control<T>['_updateFormState'],
  isRoot?: boolean,
) => {
  updateFormState(formStateData);
  const { name, ...formState } = formStateData;

  return (
    isEmptyObject(formState) ||
    Object.keys(formState).length >= Object.keys(_proxyFormState).length ||
    // 查找 _proxyFormState 是不是有 true 的属性项,如果有,则返回 true
    Object.keys(formState).find(
      (key) =>
        _proxyFormState[key as keyof ReadFormState] ===
        (!isRoot || VALIDATION_MODE.all),
    )
  );
};

总而言之,通过代理 FormState,react-hook-form 能知道哪些属性是被用户使用的,根据**是否使用到,**来决定是否 rerender。

useFormState

useFormState 没啥好说的了,它会将 rerender 控制在调用它的组件层级,性能更佳。源码如下:

function useFormState<TFieldValues extends FieldValues = FieldValues>(
  props?: UseFormStateProps<TFieldValues>,
): UseFormStateReturn<TFieldValues> {
  const methods = useFormContext<TFieldValues>();
  const { control = methods.control, disabled, name, exact } = props || {};
  const [formState, updateFormState] = React.useState(control._formState);
  const _mounted = React.useRef(true);
  const _localProxyFormState = React.useRef({
    isDirty: false,
    isLoading: false,
    dirtyFields: false,
    touchedFields: false,
    validatingFields: false,
    isValidating: false,
    isValid: false,
    errors: false,
  });
  const _name = React.useRef(name);

  _name.current = name;

  useSubscribe({
    disabled,
    next: (
      value: Partial<FormState<TFieldValues>> & { name?: InternalFieldName },
    ) =>
      _mounted.current &&
      // 注意这里,传入 name 能获得更好的性能
      shouldSubscribeByName(
        _name.current as InternalFieldName,
        value.name,
        exact,
      ) &&
      shouldRenderFormState(
        value,
        _localProxyFormState.current,
        control._updateFormState,
      ) &&
      updateFormState({
        ...control._formState,
        ...value,
      }),
    subject: control._subjects.state,
  });

  React.useEffect(() => {
    _mounted.current = true;
    _localProxyFormState.current.isValid && control._updateValid(true);

    return () => {
      _mounted.current = false;
    };
  }, [control]);

  return getProxyFormState(
    formState,
    control,
    _localProxyFormState.current,
    false,
  );
}

使用 setValue 函数,无法触发表单值的更新?

看下面的例子:

 const { fields } = useFieldArray({
    control,
    name: 'form',
  });
  return (
     <ul>
      {fields.map((item, index) => (
        <li key={item.id}>
          <Controller
            render={({ field }) => (
              <input {...field} />
            )}
            name={`form.${index}.age`}
            control={control}
          />
        </li>
      ))}
    </ul>
  )
  ...
  // 页面上无变化 ???
  setValue('form.0.age',18)

如果你认真看了上面 setValue 和 useFieldArray 的源码分析,你就会明白了。下面我简单分析一下:

  1. setValue 判断 'form.0.age' 不属于 array,调用了 control._subject.values.next()

  2. 而 useFieldArray 导出的 field 其实是依赖了 useFieldArray 内部的一个 state

  3. useFieldArray 内部对 control._subject.array 进行订阅,当control._subject.array 更新时,才会触发 state 的更新。

综上所诉,本质上是对 setValue 和 useFieldArray 的用法不够了解,导致了错误的使用姿势。正确的用法应该是尽量使用 useFieldArray 提供的 api,如 append、repalce、remove 等,或者这样去使用 setValue:

setValue('form',[{age:18},{age:15}])

至于为什么,想必不用我多说了吧。

使用 reset 无法清空表单值?

看下以下例子:

import React, { useEffect } from 'react';
import {
  Controller,
  FormProvider,
  useForm,
  useFormContext,
} from 'react-hook-form';

function Input() {
  const { control } = useFormContext();

  return (
    <Controller
      name="test"
      render={({ field: { onChange, value } }) => (
          <input
            value={value}
            onChange={(e) => {
              onChange(e.target.value);
            }}
          />
      )}
      control={control}
    />
  );
}

export const TestFormState: React.FC = () => {
  const form = useForm();
  const { reset } = form;
  return (
    <FormProvider {...form}>
      <Input></Input>
      <button onClick={reset}> reset </button>
    </FormProvider>
  );
};

当 Input 值改变时,点击 reset ,发现值并没有重置。

但当你给一个初始值时,reset 又是成功的:

const form = useForm({ defaultValues:{test:"1"} });

研究了一番发现这里其实很坑。我们来看下 Input 组件的写法:

    <Controller
      name="test"
      render={({ field: { onChange, value } }) => (
          <input
           //  reset value 值确实已经变了值为 undefined
           // 但当你将 undefined 作为值传入时是不能成功的
           // 正确做法是改成这样value={value??""}
            value={value}
            onChange={(e) => {
              onChange(e.target.value);
            }}
          />
      )}
      control={control}
    />

但如果有很多个控件,我们都要逐一改造就很麻烦,其实还有一种方法,那就是 reset 的时候传入一个值,如下:

reset({test:""});

如何监听 errors 的变化从而做复杂计算?

一般而言,errors 的使用场景如下:

在控件内部使用 error:

function Input() {
  const { control, setError } = useFormContext();
  const formState = useFormState();

  return (
    <Controller
      name="test"
      render={({ field: { onChange, value }, fieldState: { error } }) => (
        <>
          <input
            style={{ background: 'yellow', color: 'black' }}
            value={value}
            onChange={(e) => {
              onChange(e.target.value);
              setError('test', { message: e.target.value });
            }}
          />
          <div style={{ background: 'red' }}>{error?.message}</div>
        </>
      )}
      control={control}
    />
  );
}

但如果需要从整体上获取错误呢?

举个实际例子,我们需要知道有几个错误,且需要将这里错误信息收集起来,在侧边栏统一展示给用户。

我们可能会这么写:

function handleErrors(errors){
    // do something
}

export const Form: React.FC = () => {
  const form = useForm();
  const { formState:{ errors } } = form
  const { errorCount, errorMsgList } = handleErrors(errors)

  return (
    <FormProvider {...form}>
      ...
    </FormProvider>
  );
};

实际上,业务表单有可能会很复杂,这就意味着 handleErrors 的处理逻辑可能变复杂,例如你需要考虑 errros 是否是数组,以及是否有 error 嵌套的情况,这就需要对 errors 进行遍历处理。于是,你可能会这样去优化

 const { errorCount, errorMsgList } = useMemo(()=>handleErrors(errors), [errors]) 

这样改了之后,只有 errors 变化时才去做这个计算。

但这样又会引发新的问题,我来给你们捋一下:

  1. 执行了 setError 或者触发了规则校验,导致 formState 发生改变,Form 组件 rerender.

  2. formState 虽然是变了,但 formState.errors 是个不可变引用

  3. useMemo 不执行,handleErrors 不执行

所以 ,这没法达到我们想要的效果。

所幸,通过翻阅源码,我们还是能找到一些可行的优化方案。还记得上文提到过的订阅发布系统吗?我们可以利用这个机制来达成我们的优化目标。

React-hook-form 会将 _subject 实例挂在 controller 上,因此,我们可以基于这个特性实现一个自定义 hook,如下:

import React, { useEffect, useRef } from 'react';
import {
  Control,
  FieldValues,
  FormState,
  InternalFieldName,
} from 'react-hook-form';

export function useSubscribe<T>(props: Props<T>) {
  const _props = React.useRef(props);
  _props.current = props;

  React.useEffect(() => {
    const subscription =
      _props.current.subject &&
      _props.current.subject.subscribe({
        next: _props.current.next,
      });

    return () => {
      subscription && subscription.unsubscribe();
    };
  }, []);
}

export function useFormErrorsWatch(params: {
  control: Control<FieldValues, any>;
  callBack: (
    value: Partial<FormState<any>> & { name?: InternalFieldName },
  ) => void;
}) {
  const { control, callBack } = params;
  useSubscribe({
    next: callBack,
    subject: control._subjects.state as unknown as any,
  });
}

使用如下:

  useFormErrorsWatch({
    control,
    callBack: params => {
    // 这里监听的是 control._subjects.state,因此有时 error 没有更改,也会触发回调执行
    // 例如用户改动表单项,那么这个表单项的 isDirty 会设置成 true,也会执行这个回调,
    // 因此这里需要判断下 errors 是否为空
        if(params.errros) {
           handleError(params.errors)
        }
  });

这种方案非官方推荐,一般而言,非复杂场景不会用上,如果需要使用,请确保对 react-hook-form 的机制足够了解,以及做好充分测试。另外需要注意是未来 react-hook-form 版本更新所带来的 api 变动风险等。

转载自:https://juejin.cn/post/7423261843933675558
评论
请登录