react-hook-form 原理及实践通过源码阅读,带大家梳理 react-hook-form 的原理,以及分享一些
前言
最近在做一个批量编辑商品的需求,需要结合 table 和 form 使用。我们使用了 react-hook-form 作为表单方案。而在实际开发过程中,table 的列数量达到 20+,每个列都对应一个表单控件,这对性能有极大要求,使用不当的话会造成页面会卡顿,影响用户体验。因此,有必要对 react-hook-form 做一个深入的学习了解。下面我将 react-hook-form 的核心原理以及实际开发过程中踩到的坑分享出来,希望对大家有所帮助。
核心原理
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],
),
};
}
小结
这里再简单画个图来梳理一下主要流程:
这里的 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 的源码分析,你就会明白了。下面我简单分析一下:
-
setValue 判断 'form.0.age' 不属于 array,调用了 control._subject.values.next()
-
而 useFieldArray 导出的 field 其实是依赖了 useFieldArray 内部的一个 state
-
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 变化时才去做这个计算。
但这样又会引发新的问题,我来给你们捋一下:
-
执行了 setError 或者触发了规则校验,导致 formState 发生改变,Form 组件 rerender.
-
formState 虽然是变了,但 formState.errors 是个不可变引用
-
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