[RHF] useWatch가 특정 필드의 변화만 감지할 수 있는 이유
🔥 결론
✅ useWatch의 개별 useState
- 각 useWatch는 자신만의 useState를 가짐
- 초기값은
control._getWatch()
로 폼의 최신값에서 해당 name 값을 가져옴 - useWatch는 값을 읽기만 함 (폼 데이터 변경 X)
- 폼 데이터 변경은 setValue, onChange 등에서 subject.next를 호출하여 publish
✅ 구독 시스템
- useWatch는 특정 name 필드를 구독함
- 구독한 name 필드가 변경될 때만 setState(updateValue) 실행
- useWatch를 호출하는 컴포넌트만 리렌더링
✅ _subscribe 함수
- shouldSubscribeByName으로 구독한 name과 변경된 name 비교
- 일치하면 콜백 실행 → setState → 리렌더링
코드 분석
useWatch
useWatch는 내부적으로 useState로 관리하며, 초기값을 폼에서 최신 데이터를 가져옵니다.
폼 변경을 구독하고 있고, name 필드와 callback을 넘겨주고 있는데, callback이 실행되면 setState가 되는 것을 알 수 있습니다.
export function useWatch<TFieldValues extends FieldValues>(
props?: UseWatchProps<TFieldValues>
) {
const methods = useFormContext<TFieldValues>();
const {
control = methods.control,
name,
defaultValue,
disabled,
exact,
} = props || {};
const _defaultValue = React.useRef(defaultValue);
const [value, updateValue] = React.useState(
control._getWatch(
name as InternalFieldName,
_defaultValue.current as DeepPartialSkipArrayKey<TFieldValues>
)
);
// 폼 변경 구독
useIsomorphicLayoutEffect(
() =>
control._subscribe({
name,
formState: {
values: true,
},
exact,
callback: (formState) =>
!disabled &&
updateValue(
generateWatchOutput(
name as InternalFieldName | InternalFieldName[],
control._names,
formState.values || control._formValues,
false,
_defaultValue.current
)
),
}),
[name, control, disabled, exact]
);
React.useEffect(() => control._removeUnmounted());
return value;
}
_subscribe
subscribe 할 때 구독하고 싶은 필드명(name)을 전달하면 내부적으로 name 필드에 대한 방어코드를 실행합니다.
subject.next를 호출하여 subscribe하고 있는 구독자들에게 notify를 보내는데, next할 때 보내는 name필드가 formState.name에 들어가고, subscribe할 때 주입한 props.name과 비교합니다.
_subjects.state.next({
validatingFields: _formState.validatingFields,
isValidating: !isEmptyObject(_formState.validatingFields),
});
변경된 formState와 구독하고 있는 props의 name이 같을 때만 callback을 실행합니다.
따라서, useWatch는 특정 name에 대해서만 상태 업데이트를 실행해 리렌더링을 발생시키는 것을 알 수 있습니다.
const _subscribe: FromSubscribe<TFieldValues> = (props) =>
_subjects.state.subscribe({
next: (
formState: Partial<FormState<TFieldValues>> & {
name?: InternalFieldName;
values?: TFieldValues | undefined;
type?: EventType;
}
) => {
if (
// name 필드 방어 코드
shouldSubscribeByName(props.name, formState.name, props.exact) &&
shouldRenderFormState(
formState,
(props.formState as ReadFormState) || _proxyFormState,
_setFormState,
props.reRenderRoot
)
) {
props.callback({
values: { ..._formValues } as TFieldValues,
..._formState,
...formState,
});
}
},
}).unsubscribe;
const subscribe: UseFormSubscribe<TFieldValues> = (props) => {
_state.mount = true;
_proxySubscribeFormState = {
..._proxySubscribeFormState,
...props.formState,
};
return _subscribe({
...props,
formState: _proxySubscribeFormState,
});
};
댓글남기기