react-hook-form은 기본적으로 비제어 컴포넌트 방식으로 폼을 접근한다. 하지만 antd나 react-datepicker, react-select 등 기본적으로 제어 컴포넌트 요소의 경우 ref를 전달해주는 방식이 아닌 제어 요소를 다루는 방법을 사용해야 한다.
react-hook-form에서는 그런 제어 컴포넌트들을 다루기 위해 Controller
를 제공한다. 하지만 Controller
를 사용할 때, 제어 컴포넌트를 바로 사용하지 않고 하위 요소로 분리하면 일반적인(?) 방식으론 값이 제출되지 않고 undefined로 출력됐다.
처음에는 부모 컴포넌트에서 Controller를 직접 선언했는데, 이렇게 하면 undefined
를 반환했다. 따로 컴포넌트를 분리하지 않은 TimePicker의 경우 아래와 같이 바로 데이터를 전송하는 컴포넌트에다가 선언해도 잘 값이 출력됐는데, 아래와 같은 방법으론 값이 출력되지 않았다.
// undefined
<InputOther
label="label1"
component={
<Controller
as={<InputDatePicker />}
name="label1"
type="date"
control={control}
/>
}
/>
부모 컴포넌트
import InputDatePicker from "./InputDatePicker";
import { useForm } from "react-hook-form";
import "./styles.css";
import "antd/dist/antd.css";
export default function App() {
const { handleSubmit, control } = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<InputDatePicker
// control을 자식 컴포넌트에 넘겨주기
control={control}
/>
<button type="submit">전송</button>
</form>
);
}
자식 컴포넌트
import React from "react";
import { DatePicker } from "antd";
import { Controller } from "react-hook-form";
// control을 받아오고,
function InputDatePicker({ control }) {
const dateFormat = "YYYY-MM-DD";
return (
// Controller를 선언한 후 control을 속성으로 넣어주면 된다.
<Controller
control={control}
name="installDate"
format={dateFormat}
// render를 사용해서, field값을 복사하거나 꺼내 쓰면 된다.
// field안에는 value나 onBlur와 같은 함수도 있음
// render안의 onChange를 조작해, onChange안에 들어갈 값을
// 선택할 수 있다.
render={({ field: { onChange } }) => (
// antd의 datepicker에서 e.target.value는
// moment 객체 그대로를 반환하기에,
// "2021-04-15"와 같은 값을 얻고싶다면, 두번째 파라미터
// "dateString"을 추가해서 값을 넣어야 한다.
<DatePicker
onChange={(value, dateString) => {
onChange(dateString);
}}
format={dateFormat}
/>
)}
/>
);
}
export default InputDatePicker;
잘 출력되는 모습 👏👏 !!
Controller
를 부모 컴포넌트에서 사용하지 않고, 자식 컴포넌트에서 사용하고 control
를 전달하는게 포인트이다.
분명 위의 codesandbox의 예제에서는 제대로 작동됐는데, 내가 진행중인 프로젝트에선 field의 값을 확인해보면 undefined로 출력됐다. 위의 코드에서 달라진 부분이 없고, 도대체 뭐가 문제인지 몰라서 끙끙 헤맸는데 이 문제는 버전문제였다.
진행중인 프로젝트에서는 "react-hook-form": "^6.13.1",
버전을 쓰고 있었는데, 위의 field
와 render
를 사용하는 기능은 최신 버전인 7.13.0
에서 사용이 되고 있었다.
그런데, 7.13.0
버전으로 업그레이드 하면 기존에 사용되던
<Input label="label1" name="label1" register={register} />
와 같이 register를 ref로 전달해주는 방식에서 path.split is not a function ~~ 하는 에러가 생긴다.
해당 문제를 해결하려면, 위와 같은 형식으로, register를 전달시키는게 아닌
<Input label="label1" name="label1" {...register("label1")} />
헤당 형식으로 바꿔줘야 했었다.
하지만 field
를 사용하자고 모든 폼을 바꾸는건 정말정말정말..수가 많아서 6.13.1버전에서 onChange
메서드를 사용하는 방법을 찾아봤는데, 아래와 같은 방법을 사용하면 됐다.
<Controller
control={control}
name="date"
format={dateFormat}
defaultValue={date}
render={({ onChange }) => (
<DatePicker onChange={(value, dateString) => onChange(dateString)} />
)}
/>
as
를 사용해서 onChange
메서드를 Controller
에 직접 지정하거나, render
를 사용한 뒤 onChange
를 직접 비구조화 할당으로 가져와 render
내부에서 사용하면 된다.
잘 전송되는 감격의 모습...
// 버전 6.13.1
- <Controller as={Input}
- name="test"
- onChange={([_, value]) => value}
- onChangeName="onTextChange"
- onBlur={([_, value]) => value}
- onBlurName="onTextChange"
- valueName="textValue"
- />
// 버전 7.13.0
+ <Controller name="test"
+ render={({ onChange, onBlur, value }) => {
+ return <Input
+ valueName={value}
+ onTextChange={(val) => onChange(value)}
+ onTextBlur={(val) => onBlur(value)}
+ />
+ }}
+ />
as로 요소를 가져오면, onChange
와 onBlur
와 같은 다양한 메서드를 하나씩 지정해줘야 하는 반면 render를 사용하면 field
나 위의 예시와 같이 조금 더 쉽게 지정할 수 있다.