[ 살펴보기 ] React Hook Form - useController
![[ 살펴보기 ] React Hook Form - useController](https://cdn.hashnode.com/res/hashnode/image/upload/v1723187514441/13867278-0baa-404e-967d-bb8074c33478.png)
만약 Material UI ( MUI )와 같은 UI Library를 사용 중이라면 Controller, useController를 통해 UI Library가 제공하는 Input component를 useForm에 등록하여 사용할 수 있다.
해당 포스트에선 MUI를 기반으로 Controller, useController를 통해 Input component를 등록하여 관리하는 방법을 살펴본다.
우선 Controller를 통해 MUI input component를 등록하는 방법을 살펴보자.
import {
Controller,
useForm,
} from "react-hook-form";
import { TextField } from "@mui/material";
const {
control,
...
} = useForm<MemberForm>({
defaultValues: {
address: "",
age: "",
city: "",
},
});
<Controller
control={control}
name="address"
rules={{ minLength: { value: 3, message: "minLength error" } }}
shouldUnregister={true}
render={({
field: { onChange, onBlur, value },
fieldState: { invalid, isDirty, isTouched, isValidating, error },
formState: { dirtyFields, disabled, errors },
}) => {
return (
<TextField
variant="filled"
value={value}
onChange={onChange}
onBlur={onBlur}
/>
);
}}
/>
Controller component에 전달하는 주요 props은 다음과 같다
control : useForm이 return하는 control 객체를 전달해 Controller가 render하는 input component를 등록한다.
name : input의 name을 전달한다.
render : useForm에 등록하고자 하는 input component를 return하는 function을 전달한다.
rules : input validaton 규칙을 정한다.
shouldUnregister : input이 unmount되었을 때 useForm에서도 unregister되어야 할지 여부를 정한다.
render에 전달하는 function은 다음 arguments를 전달받는다
field : onChange, onBlur, value, ref와 같이 연동할 Input component에 전달할 property들을 가지고 있다
fieldState : isValid, isDirty, error등 현재 input에 대한 정보를 가지고 있다. 여기서 isDirty는 form 전체의 dirty여부가 아닌 해당 input에 대한 dirty 여부이다.
formState : dirtyFields, touchedFields, defaultValues, errors등 form 전체에 대한 정보를 가지고 있다.
한 가지 기억해야 할 특징은 Controller의 render 함수는 다음과 같은 상황에서 재실행된다.
Controller의 render prop에 전달된 함수의 fieldState param중 isValidating을 사용하고 있고 같은 useForm 아래 관리되는 input의 값이 변경되었을 때
Controller와 연결된 input의 value가 update될 때
control property를 전달받은 useForm의 dirtyFields, touchedFields, error와 같은 값의 상태가 변경되면 useForm을 사용하는 component가 re-render되므로 Controller 역시 re-render 되며 render 함수는 다시 실행된다.
useController
위에서 살펴본 Controll component 대신 useController를 통해 바로 MUI Input component를 바로 등록하여 사용할 수 있다.
const {
control,
...
} = useForm<MemberForm>({
defaultValues: {
address: "",
age: "",
city: "",
},
});
const {
field: { onChange, onBlur, value, name },
fieldState: { invalid, isDirty, isTouched, isValidating, error },
formState: { dirtyFields, disabled, errors },
} = useController({
control, // useForm의 control을 전달하여 연결
name: "address",
rules: { minLength: { value: 3, message: "minLength error" } },
});
<TextField
variant="filled"
name={name}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
위의 예제에서 볼 수 있듯이 useController는 form을 관리하는 useForm의 control 객체를 전달받아 useForm과 연결한다. 그리고 useController hook이 return하는 value와 event handler를 Input component에 전달하여 Input component를 관리한다.
그리고 하나의 useController는 하나의 input element와 연동된다. 그리고 useController hook을 사용하는 component는 다음과 같은 상황에서 re-render된다.
useController가 return하는 fieldState의 isValidating을 사용하고 있고 같은 useForm 아래 관리되는 input의 값이 변경되었을 때
useController에 연결된 input의 value가 update될 때
control property를 전달받은 useForm이 부모 component에 존재하고 dirtyFields, touchedFields, error와 같은 값의 상태가 변경되면 useForm을 사용하는 부모 component에서 re-render가 발생하므로 자식 component 역시 re-render가 발생한다. 다만 여기서 memo를 통해 자식 component를 memorizing해주면 위의 상황에서 자식 component는 re-render가 발생하진 않으며 ( 해당 component로 전달하는 모든 prop에 변화가 없다는 가정하에 ) useController로 연결된 input이 update 발생할 때만 다시 re-render가 발생한다
위와 같은 특성이 있기에 useController는 특정 input component와 useController hook을 별개의 컴포넌트로 떼내어 한 쌍으로 묶어 사용하기 적합하다.
추가로 MUI의 다양한 Input component에 useController를 연동하는 방법을 예제로 살펴보자. 예제에서 control은 부모 component에 선언된 useForm을 통해 전달 받는다고 가정한다
TextField
...
import { Control, useController } from "react-hook-form";
import { TextField } from "@mui/material";
type Props = {
control: Control<MemberForm>;
};
const ControlledInput = ({ control }: Props) => {
const {
field: { onChange, onBlur, value, name },
} = useController({
control,
name: "address",
});
return (
<TextField
variant="filled"
name={name}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
);
};
export default ControlledInput;
Select
...
import { Control, useController } from "react-hook-form";
import { MenuItem, Select } from "@mui/material";
type Props = {
control: Control<MemberForm>;
};
const ControlledInput = ({ control }: Props) => {
const {
field: { onChange, value, name },
} = useController({
control,
name: "city",
});
return (
<Select label="City" name={name} value={value} onChange={onChange}>
<MenuItem value="busan">Busan</MenuItem>
<MenuItem value="seoul">Seoul</MenuItem>
</Select>
);
};
export default ControlledInput;
Radio
...
import {
FormControl,
FormControlLabel,
FormLabel,
Radio,
RadioGroup,
} from "@mui/material";
import { Control, useController } from "react-hook-form";
type Props = {
control: Control<MemberForm>;
};
const ControlledInput = ({ control }: Props) => {
const {
field: { onChange, onBlur, value, name },
} = useController({
control,
name: "city",
});
return (
<FormControl>
<FormLabel id="input-city">City</FormLabel>
<RadioGroup
aria-labelledby="input-city"
name={name}
value={value}
onChange={onChange}
>
<FormControlLabel value="busan" control={<Radio />} label="Busan" />
<FormControlLabel value="seoul" control={<Radio />} label="Seoul" />
</RadioGroup>
</FormControl>
);
};
export default ControlledInput;
Checkbox
...
import {
Checkbox,
CheckboxProps,
FormControlLabel,
FormGroup,
} from "@mui/material";
import { Control, useController } from "react-hook-form";
type Props = {
control: Control<MemberForm>;
};
const ControlledInput = ({ control }: Props) => {
const {
field: { onChange, onBlur, value, name },
} = useController({
control,
name: "hobbies",
});
const handleCheckboxState: CheckboxProps["onChange"] = (e, checked) => {
const newHobby = e.target.value;
if (!checked) {
const newHobbyList = value.filter((item) => item !== newHobby);
onChange(newHobbyList);
return;
}
onChange([...value, newHobby]);
};
return (
<FormGroup>
<FormControlLabel
label="Sport"
control={
<Checkbox
value="sport"
checked={value?.includes("sport")}
onChange={handleCheckboxState}
/>
}
/>
<FormControlLabel
label="Cooking"
control={
<Checkbox
value="cooking"
checked={value?.includes("cooking")}
onChange={handleCheckboxState}
/>
}
/>
</FormGroup>
);
};
export default ControlledInput;
![[ 살펴보기 ] RDB - Relationships](https://cdn.hashnode.com/res/hashnode/image/upload/v1739711556668/48dc9e84-a621-42aa-9c9f-5fc5c436f0ec.jpeg)
![[ 살펴보기 ] MySQL - Data types](https://cdn.hashnode.com/res/hashnode/image/upload/v1739593589113/530f8704-4d27-42c9-a451-bb5c63150b99.jpeg)
![[ 살펴보기 ] TypeORM - Transactions, Migration](https://cdn.hashnode.com/res/hashnode/image/upload/v1739106042581/980b8133-61d4-406a-a026-65be9c28eace.jpeg)
![[ 살펴보기 ] TypeORM - Relations](https://cdn.hashnode.com/res/hashnode/image/upload/v1738666874402/b688bd0b-b6bb-4f43-87d8-c1b46b59f1b7.jpeg)
![[ 살펴보기 ] TypeORM - Basics](https://cdn.hashnode.com/res/hashnode/image/upload/v1738666803591/bef5df17-7dc7-4123-ae55-004d5042df39.jpeg)