I ran into react-hook-form
when I yarn shadcn@latest add form
. There were too many components kicking in like <Form />
, <FormField />
, <FormControl />
, <FormLabel />
, and <FormMessage />
. I didn't get the hang of it while I digged into it at that time. If you're struggling with Radix Primitive or any headless library and you want to make it work together with react-hook-form, you'll get the look and feel of it here.
The Classic Agenda: Controlled Component and Uncontrolled Component
It all comes down to where you manage the state. If DOM is managing state and you need to access state using ref
, it's uncontrolled. If react is managing state and you need to update it whenever some event fires, it's controlled.
Please read the legacy docs for more details.
register()
register()
maanges state in DOM, and the component can be viewed as uncontrolled component.
Controller
<Controller />
manages state in react, and the component can be viewed as controlled component.
useController
useContrller()
lets you manage the state with a hook.
register()
vs. Controller()
- If you need to build UIs like
Slider
with min-max value,Rating
, orColor Picker
, the value of these elements can't be decribed as classic HTML native elements.
Slider.tsx
export function PriceRangeSlider() {
const { control } = useFormContext();
const { field: { value, onChange } = useController({
name: 'priceRange',
control,
});
const handleLeft = (e: ChangeEvent) => {
onChange([ Number(e.target.value), value[1] ]);
}
const handleRight = (e: ChangeEvent) => {
onChange([ value[0], Number(e.target.value) ];
}
return <Slider onLeft={handleLeft} onRight={handleRight} />
}
If the state needs to be known globally, you should use controller instead of register. For example, every UI changes depending on theme(dark mode/light mode). This state needs to be injected to every component in react, so you better use controller for this.
Here's the example code of theme context injection. In
<ThemeContext />
, we can define context hook and provider altogether.<ThemeRow />
is a component that updates the theme state. Like<Card />
we can inject theme state to the rest of the application.
ThemeContext.tsx
type Theme = 'light' | 'dark';
interface ThemeContext {
theme: Theme;
toggle: () => void;
}
const [Provider, useContext] = buildContext<>('ThemeContext', null);
interface ThemeProviderProps {
children: ReactNode;
}
export function ThemeProvider({ children }: ) {
const [theme, setTheme] = useStorageState<'dark' | 'light'>('theme', {
defaultValue: 'dark',
});
const toggle = () => {
setTheme((prev) => prev === 'dark' ? 'light' : 'dark');
}
return <Provider theme={theme} toggle={toggle}>{children}</Provider/>
}
export useTheme = () => useContext();
ThemeRow.tsx
export function ThemeRow() {
const { theme, toggle } = useTheme();
return <Stack.Vertical width="full" justify="space-between" align="center" padding={16}>
<Label>{theme === 'dark' ? '라이트 모드' : '다크 모드'}로 전환</Label>
<Toggle theme={theme} toggle={toggle} />
</Stack.Vertical>
}
Card.tsx
interface Props {
title?: ReactNode;
content?: ReactNode;
footer?: ReactNode;
}
export function Card({ header, body, footer }: Props) {
const { theme } = useTheme();
return <Stack.Horizontal css={{
backgroundColor: theme === 'dark' ? colors.dark : colors.white
}}>
{header}
{header != null && <Divider />
<div>
<Spacing size={24} />
{body}
<Spacing size={24} />
{footer != null && <Divider />
{footer}
</Stack.Horizontal>
}
Controler vs. useController
Your JSX looks like bloating like my stomache sometimes when you keep using <Controller />
.
It minimizes the eye movement.
However, if there are a lot of fields in the same component, using multiple useController()
looks confusing and verbose. It becomes more difficult to find props in similar declarations. I use 'Controller' to avoid confusion even though jsx looks bloating. If you could split the component into multiple ones, it's a better strategy.
Top comments (0)