Radix-UI Select + React Hook Form 사용시 ref 전달 처리 에러 대응
Radix-UI
와 React Hook Form
을 처음 써보면서 삽질한 기록입니다.
기본적으로 Radix-UI는 headless UI Component
컨셉으로 만들어진 라이브러리이지만, @radix-ui/themes
를 이용하면 기본적인 스타일은 적용되어있기 때문에 편리합니다.
React Hook Form
도 처음 써보는데, Radix UI
와 함께 쓰다보니 소소한 문제가 좀 있습니다.
HTML
에서 보여지는 form
요소들은 대체로 좀 못생겼습니다. 스타일이 적용되지 않는 구성요소들도 있는데, 그중에 대표적인게 select
태그입니다.
그래서 스타일을 입히려고 하면 우회해서 처리하는 방법들을 써야합니다.
그러다보니 Radix UI
의 Select
컴포넌트도 대략 흝어보니 ref
를 이용해서 하위 컴포넌트로 값을 전달하는 방식인 것 같습니다.
이런 방식으로 쓰이는 컴포넌트에 대응하기 위해서 React hook form
에서도 ref
를 전달해줄 수 있습니다. 그런데 Select.Root
컴포넌트에 ref
를 그대로 전달해주면 문제가 좀 생깁니다.
그래서 이 부분을 해결해보고자 Chat GPT
에 물어봤더니 아래와 같은 답을 내주더군요.
Radix UI의 Select 컴포넌트와 React Hook Form간의 ref 에러 해결 방안
Radix UI
의 Select
컴포넌트를 사용할 때 react-hook-form
에서 ref
에러가 발생하는 이유는 주로 Radix UI
와 react-hook-form
간의 참조(ref) 호환성 문제 때문입니다. react-hook-form
은 주로 네이티브 HTML 폼 요소들과 직접적으로 연동되도록 설계되어 있으며, 이러한 요소들은 ref
속성을 사용하여 DOM 요소에 직접 접근합니다.
그러나 Radix UI
와 같은 커스텀 컴포넌트 라이브러리들은 네이티브 HTML 요소를 직접 노출시키지 않을 수 있으며, 이는 react-hook-form
의 register
함수와 해당 컴포넌트의 ref
를 연결하는 데 문제를 일으킬 수 있습니다. 결과적으로, react-hook-form
이 해당 컴포넌트의 인스턴스에 접근할 수 없어 ref
에러가 발생합니다.
이 문제를 해결하기 위한 몇 가지 방법은 다음과 같습니다:
- Controller 사용:
react-hook-form
의Controller
컴포넌트를 사용하여Radix UI
의Select
컴포넌트를 감싸고,Controller
가Select
컴포넌트와의 상호작용을 관리하도록 할 수 있습니다. 이 방법은react-hook-form
이 제공하는 컨트롤러를 통해 비표준 폼 요소와의 통합을 용이하게 합니다. - forwardRef 사용:
Radix UI
컴포넌트를 감싸는 래퍼 컴포넌트를 만들고,React.forwardRef
를 사용하여 내부적으로ref
를 전달할 수 있습니다. 이렇게 하면react-hook-form
이 필요한ref
에 접근할 수 있게 됩니다. - onValueChange 사용:
Radix UI
의Select
컴포넌트에서onValueChange
이벤트를 사용하여 선택된 값을react-hook-form
의setValue
메소드를 통해 수동으로 설정할 수 있습니다. 이 방법은ref
를 사용하지 않고도 폼의 상태를 관리할 수 있게 해줍니다.
여러가지 방법을 소개해줬는데, 단순하게 해결되지는 않더군요.
1,2,3번을 모두 각각 시도해도 잘 되지 않아서, Controller
와 forwardRef
를 이용하는 1,2번을 조합해서 시도했음에도 잘 안됐습니다. 그러다가 아래 글에서 힌트를 좀 얻었어요
그 결과가 아래 소스코드입니다.
// CustomSelect.jsx
import React from "react"
import { Select } from "@radix-ui/themes"
const CustomSelect = React.forwardRef((props, ref) => (
<Select.Root {...props}>
<Select.Trigger />
{/* ref를 Select.Root가 아닌 Select.Content에 전달 */}
<Select.Content position="popper" ref={ref}>
<Select.Item value="option1">Option 1</Select.Item>
<Select.Item value="option2">Option 2</Select.Item>
</Select.Content>
</Select.Root>
))
export default CustomSelect
// Form.jsx
import {Controller, useForm} from "react-hook-form"
import CustomSelect from "@/app/ui/CustomSelect"
const Form = () => {
const {
register,
handleSubmit,
watch,
control,
formState: {errors},
} = useForm({
defaultValues: {
option: "option1",
},
})
const submitHandler = async (data) => {
console.log(data)
}
return (
<form onSubmit={handleSubmit(submitHandler)}>
<Controller
name="option"
control={control}
render={({field}) => (
{/* onValueChange를 CustomSelect 컴포넌트 외부에서 연결*/}
<CustomSelect onValueChange={field.onChange} {...field} />
)}
/>
<Button variant={"surface"} radius={"small"} type={"submit"} size={"3"}>
제출
</Button>
</form>
)
}
export default Form
보통 해결 방안으로 알려주는 것들은 모든 props
를 SelectComponent
안에서 받아서 처리하도록 나오는데, onValueChange
를 컴포넌트 외부에서 적용해주고, ref
는 Select.Root
컴포넌트가 아닌 Select.Content
에 전달해주었습니다.
오류메세지는 출력되지 않고 제대로 값이 전달되고 있기 때문이 이렇게 일단 적용하는데, 정확한 부분은 차차 더 뜯어봐야 할 것 같습니다.