Controller컴포넌트는 외부 컨트롤된 컴포넌트 (AntD, Material-UI 등)와 함께 작동하기 위해 설계된 래퍼 컴포넌트인데, 기본적으로 react-hook-form은 비제어 컴포넌트를 사용하는 방식이지만 외부라이브러리 컴포넌트는 제어컴포넌트임.
그래서 Controller는 제어 컴포넌트를 쓰기위한, 쓸때 다시 화면에 렌더링을 하기 위한 컴포넌트.
import { Select } from "antd";
import { FC } from "react";
import { Controller, useController, useFormContext } from "react-hook-form";
import RHForm from "./RHForm";
import { z } from "zod";
import { ErrorMessage } from "@hookform/error-message";
type OptionsType = {
value: string;
label: string;
};
type RHSelect1Props = {
options: OptionsType[];
name: string;
};
const RHSelect1: FC<RHSelect1Props> = ({ options, name }) => {
return (
<Controller
name={name}
render={(methods) => {
return (
<>
<Select
options={options}
onChange={methods.field.onChange}
onBlur={methods.field.onBlur}
value={methods.field.value}
ref={methods.field.ref}
/>
<ErrorMessage name={name} errors={methods.formState.errors} />
</>
);
}}
/>
);
};
const options: OptionsType[] = [
{ value: "1", label: "1" },
{ value: "2", label: "2" },
{ value: "3", label: "3" },
];
const schema = z.object({
select1: z.string({
invalid_type_error: "선택해주셈",
required_error: "선택해주셈",
}).optional()
});
type FormType = z.infer<typeof schema>;
const defaultValues: FormType = {
select1:undefined,
select2:undefined
}
const ResetButton = () => {
const methods = useFormContext<FormType>(); // 현재 폼 컨텍스트에 접근
return (
<button
type="button"
onClick={() => methods.reset(defaultValues)}
>
reset
</button>
);
};
const SelectTest = () => {
return (
<RHForm<FormType> // 엄격한 타입처리
schema={schema}
defaultValues={defaultValues}
>
<RHSelect1 options={options} name="select1" />
<button type="submit">Submit</button>
<ResetButton />
</RHForm>
);
};
export default SelectTest;
Controller의 name속성은 폼 필드의 name과 연결되고,
이전 포스팅에 사용한 RHForm 컴포넌트에 FormProvider속성으로 인해, Controller의 render속성에 field객체를 사용할 수 있다.
Controller의 onChange속성은 form의 해당 필드값을 업데이트 하는데 사용
JSX내에 직접사용하는 Controller컴포넌트와 달리, useController는 훅으로서 사용한다.
두 방법 모두 폼 필드 관리라는 목적을 가지미나 useController는 더 많은 커스터마이징 기능을 가능하게하여 유연성이 높다는 차이정도가 있다.
...
const RHSelect2: FC<RHSelect1Props> = ({ options, name }) => {
const methods = useController({
name,
});
return (
<>
<Select
options={options}
onChange={methods.field.onChange}
onBlur={methods.field.onBlur}
value={methods.field.value}
ref={methods.field.ref}
/>
<ErrorMessage name={name} errors={methods.formState.errors} />
</>
);
};
const schema = z.object({
select1: z.string({
invalid_type_error: "선택해주셈",
required_error: "선택해주셈",
}).optional(),
select2: z.string({
invalid_type_error: "선택해주셈",
required_error: "선택해주셈",
}).optional(),
});
type FormType = z.infer<typeof schema>;
const SelectTest = () => {
return (
<RHForm<FormType>
schema={schema}
defaultValues={defaultValues}
>
<RHSelect1 options={options} name="select1" />
<RHSelect2 options={options} name="select2" />
<button type="submit">Submit</button>
<ResetButton />
</RHForm>
);
};
두 방법 모두 가능하지만, Controller컴포넌트가 선언적 프로그래밍 방식에 좀 더 적합해서 나는 해당 방식을 더 선호한다.