----이글은 제가 잘 모를 때 삽질한걸 작성한 글입니다. 우연히 통계를 봤는데 이게 젤 높네요;; 공부하면서 수정하겠습니다.----
상황
난 react-hook-form을 이용해 프로젝트내에서 체크박스 폼을 구현하는 중 이상한 오류를 발생했다.
input type=checkbox에서 onChange함수를 커스텀할 일이 생겼다.
const {field} = useController(myformName)
const onChange = (e) => {
// logic...
// if logic passed,
field.onChange(!e.target.checked)
}
하지만, 내 체크박스에 이상한 일이 발생했다.
defaultValue에 true로 설정한 것이 초기에 체크되지 않은 것이다. submit을 한다면 제대로 적용됐긴한데.. 무슨일일까?
간단하게
콘솔로그를 확인해보면 분명 Bar의 a는 체크안돼있는데 true이다.(조작한 상황이 아니다. 저 상태에서 submit했다.)
코드가 어떻게 돼있길래?
코드
// App.tsx
import Bar from "./component/Bar";
import Foo from "./component/Foo";
function App() {
return (
<>
<Foo />
<Bar />
</>
);
}
export default App;
//Foo.tsx
import { useForm } from "react-hook-form";
const Foo = () => {
const foo = useForm({ defaultValues: { a: true, b: false, c: false } });
const onSubmit = (data: unknown) => {
console.log('foo',data);
};
return (
<form onSubmit={foo.handleSubmit(onSubmit)}>
<label>
<input type="checkbox" {...foo.register("a")} />a
</label>
<label>
<input type="checkbox" {...foo.register("b")} />b
</label>
<label>
<input type="checkbox" {...foo.register("c")} />c
</label>
<button type="submit">submitFoo</button>
</form>
);
};
export default Foo;
foo는 useForm을 이용해 폼을 관리한다.
//Bar.tsx
import { FormProvider, useController, useForm } from "react-hook-form";
const Bar = () => {
const bar = useForm({ defaultValues: { a: true, b: false, c: false } });
const { field: aField } = useController({ name: "a", control: bar.control });
const { field: bField } = useController({ name: "b", control: bar.control });
const { field: cField } = useController({ name: "c", control: bar.control });
const onSubmit = (data: unknown) => {
console.log("bar", data);
};
return (
<FormProvider {...bar}>
<form onSubmit={bar.handleSubmit(onSubmit)}>
<label>
<input
type="checkbox"
onChange={aField.onChange}
onBlur={aField.onBlur}
value={aField.value as unknown as string}
name={aField.name}
ref={aField.ref}
/>
a
</label>
<label>
<input
type="checkbox"
onChange={bField.onChange}
onBlur={bField.onBlur}
value={bField.value as unknown as string}
name={bField.name}
ref={bField.ref}
/>
b
</label>
<label>
<input
type="checkbox"
onChange={cField.onChange}
onBlur={cField.onBlur}
value={cField.value as unknown as string}
name={cField.name}
ref={cField.ref}
/>
c
</label>
<button type="submit">submitBar</button>
</form>
</FormProvider>
);
};
export default Bar;
bar은 useController를 이용해 form을 관리한다.
foo는 register(name)을 input에 적용하고,
bar은 {field} = useController(name)을 적용했다.
사용하는 방법은 거의 똑같아 보인다. (ts때문에 저렇게 작성했지 {...field}이런식으로 작성해도 동작한다)
무슨 차이가 있는 것일까?
궁금해서 useForm과 useController를 이참에 뜯어보기로 했다.
field vs register
콘솔로그로 둘에 무엇이 들어있는지 뜯어보자.
필드엔 value가 들어가 있다.
onBlur onChange ref도 똑같은 걸까?
useController의 내부
(props: https://react-hook-form.com/docs/usecontroller 에서 확인)
// 일부 생략
export function useController(props){
const methods = useFormContext();
const { name, disabled, control = methods.control, shouldUnregister } = props;
const value = useWatch({
control,
name,
defaultValue: get(
control._formValues,
name,
get(control._defaultValues, name, props.defaultValue)
),
exact: true,
}
const _registerProps = React.useRef(
control.register(name, {
...props.rules,
value,
...(isBoolean(props.disabled) ? { disabled: props.disabled } : {}),
})
);
return {
field: {
name,
value,
...(isBoolean(disabled) || formState.disabled
? { disabled: formState.disabled || disabled }
: {}),
onChange: React.useCallback(
(event) =>
_registerProps.current.onChange({
target: {
value: getEventValue(event),
name: name as InternalFieldName,
},
type: EVENTS.CHANGE,
}),
[name]
),
onBlur: React.useCallback(
() =>
_registerProps.current.onBlur({
target: {
value: get(control._formValues, name),
name: name as InternalFieldName,
},
type: EVENTS.BLUR,
}),
[name, control]
),
ref: (elm) => {
const field = get(control._fields, name);
if (field && elm) {
field._f.ref = {
focus: () => elm.focus(),
select: () => elm.select(),
setCustomValidity: (message: string) =>
elm.setCustomValidity(message),
reportValidity: () => elm.reportValidity(),
};
}
},
},
};
}
1. name은 props로 전달하고, value는 useWatch로 불러온다. defaultValue를 할당시킨다.
2. registerProps
methods는 useForm의 리턴값이다.
control은 methods.control이다.
registerProps는 contorl.register이다.
파악을 위해 useForm을 뜯어보자.
// 실제론 더 복잡한 코드
export function useForm(props) {
const _formControl = React.useRef();
const [formState, updateFormState] = React.useState({
defaultValues: isFunction(props.defaultValues)
? undefined
: props.defaultValues,
});
if (!_formControl.current) {
_formControl.current = {
...createFormControl(props, () =>
updateFormState((formState) => ({ ...formState }))
),
formState,
};
}
const control = _formControl.current.control;
control._options = props;
_formControl.current.formState = getProxyFormState(formState, control);
return _formControl.current;
}
method = _formControl.current이다.
_formControl.current는 createFormState란 핵심로직이 필요하다.
createFormState는 1000줄이 넘는 복잡한 코드이므로 우선 리턴값만 확인해보자.
return {
control: {
register,
unregister,
getFieldState,
handleSubmit,
setError,
_executeSchema,
_getWatch,
_getDirty,
_updateValid,
_removeUnmounted,
_updateFieldArray,
_updateDisabledField,
_getFieldArray,
_reset,
_resetDefaultValues,
_updateFormState,
_disableForm,
_subjects,
_proxyFormState,
_setErrors,
get _fields() {
return _fields;
},
get _formValues() {
return _formValues;
},
get _state() {
return _state;
},
set _state(value) {
_state = value;
},
get _defaultValues() {
return _defaultValues;
},
get _names() {
return _names;
},
set _names(value) {
_names = value;
},
get _formState() {
return _formState;
},
set _formState(value) {
_formState = value;
},
get _options() {
return _options;
},
set _options(value) {
_options = {
..._options,
...value,
};
},
},
trigger,
register,
handleSubmit,
watch,
setValue,
getValues,
reset,
resetField,
clearErrors,
unregister,
setError,
setFocus,
getFieldState,
};
control.register는 register과 동일하단 것을 알 수 있다.
정리하자면, registerProps = contorl.register = method.register = _formControl.current.register
3. 따라서 useController의 onBlur와 onChange는 결국 useFormReturnMethods.register(name)의 onBlur와 onChange에 useCallback을 씌웠을 뿐 결론적으로는 동일한 동작을 할 것이다.
4. 유심해 봐야할건 ref이다.
const foo = useForm({ defaultValues: { a: true, b: false, c: false } });
return(
<label>
<input type="checkbox" ref={foo.register('a').ref}/>a
</label>
)
이렇게만 해도 해당 인풋은 체크돼있다.
//usecontroller ref
ref: (elm) => {
const field = get(control._fields, name);
if (field && elm) {
field._f.ref = {
focus: () => elm.focus(),
select: () => elm.select(),
setCustomValidity: (message: string) =>
elm.setCustomValidity(message),
reportValidity: () => elm.reportValidity(),
};
}
},
여기서 우린 우리가 useController의 ref로 넣어줄 콜백함수에서
elm ref와 포커스, 내용선택, 유효성검사등 일부기능만 연결시켜주는 것을 볼 수 있다.
따라서 useController의 ref는 useForm ref와 달리 일부기능만 사용할 수 있다.
체크박스의 경우 createFormcontroller에서 해당 내용을 확인할 수 있다.
https://github.com/react-hook-form/react-hook-form/blob/master/src/logic/createFormControl.tsd
아직 내공이 부족해서 해당 코드에 대한 분석은 어려울 것 같다.
'개발' 카테고리의 다른 글
nextjs login ui 고민이 있다. (0) | 2024.02.28 |
---|---|
내가 개발중인 nestjs 인증 structure (1) | 2024.02.27 |
github oauth2 클라이언트와 서버 구현해보기 -3 (1) | 2024.02.15 |
github oauth2 클라이언트와 서버 구현해보기 -2 (1) | 2024.02.15 |
bcrypt invalid ELF header 에러 해결하기 (0) | 2024.02.14 |