DEV Community

Chan
Chan

Posted on

Controller, useControlller(), and register() in react-hook-form

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, or Color 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} />
}

Enter fullscreen mode Exit fullscreen mode
  • 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();

Enter fullscreen mode Exit fullscreen mode

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>
}

Enter fullscreen mode Exit fullscreen mode

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>

}

Enter fullscreen mode Exit fullscreen mode

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)