skip to content

In a React Hook Form signup form, how do you connect a controlled date-picker component that takes value and onChange but exposes no native input?

level: middleimportance: must knowfreq 60%

answer

  1. register needs a DOM input
  2. render prop with field
  3. value, onChange, onBlur, ref
  4. default null, not undefined

basics

~10 s

Wrap the date-picker in React Hook Form's Controller (or call useController) with name, control and rules. Its render prop supplies field.value, field.onChange, field.onBlur and field.ref, which you map onto the picker's own props.

solid answer

~40 s

`register` only works when there is a native element whose value RHF can read through a ref. A date-picker from a component library is **controlled**: it takes a `value` and calls `onChange(date)`. `<Controller name="birthDate" control={control} rules={{ required: 'Pick a date' }} render={({ field, fieldState }) => ...} />` bridges the two: map `field.value` to the picker's value prop, `field.onChange` to its change callback, `field.onBlur` to its blur callback so the field can be marked touched, and `field.ref` to whatever focusable ref the picker forwards so RHF can focus it on error. `fieldState.error` gives that field's error. Give the field a default in `useForm`'s `defaultValues` — `null`, not `undefined` — and never also `register` the same name.

code

tsx · 34 lines
tsx
import { Controller, useForm } from 'react-hook-form';
import { DatePicker } from './DatePicker'; // a controlled widget: value, onChange(Date | null), onBlur, inputRef

type Signup = { email: string; birthDate: Date | null };

export function SignupForm({ onSignup }: { onSignup: (s: Signup) => void }) {
  const { register, control, handleSubmit } = useForm<Signup>({
    defaultValues: { email: '', birthDate: null },
    mode: 'onTouched',
  });

  return (
    <form onSubmit={handleSubmit(onSignup)}>
      <input {...register('email', { required: 'Email is required' })} />
      <Controller
        name="birthDate"
        control={control}
        rules={{ required: 'Pick your date of birth' }}
        render={({ field, fieldState }) => (
          <>
            <DatePicker
              value={field.value}
              onChange={field.onChange}
              onBlur={field.onBlur}
              inputRef={field.ref}
            />
            {fieldState.error && <p role="alert">{fieldState.error.message}</p>}
          </>
        )}
      />
      <button>Create account</button>
    </form>
  );
}

go deeper

for a junior

Know that Controller exists for components that are not native inputs, and the four field props you pass through.

for a middle

Explain why register cannot see a controlled widget's value, and what onBlur and ref each enable.

for a senior

Wrap widgets once with useController in a shared input kit, handle null defaults and value transforms, and keep focus-on-error working.

for a principal

Decide how a design system's controlled widgets should expose refs and blur so form libraries integrate without per-screen glue.

## Why register is not enough here React Hook Form's default path is `register`, which assumes a **native element**: RHF attaches a `ref`, listens to its change and blur events, and reads `event.target.value`. Many real widgets do not fit that shape. A calendar date-picker, a searchable select or a rich-text editor from a component library is typically **controlled** — it wants a `value` prop and reports changes through a callback such as `onChange(date)` that receives a `Date`, not a DOM event. For those, RHF provides **`Controller`** (a component) and **`useController`** (the hook it is built on). They register the field for you and hand you a controlled interface. ## What the render prop gives you `Controller` takes `name`, `control` (from `useForm`, optional under `FormProvider`), optional `rules` in the same format as `register`'s options, and a `render` function that receives three objects: | Object | Members | Use | |---|---|---| | `field` | `value`, `onChange`, `onBlur`, `name`, `ref`, `disabled` | Wire onto the widget's props | | `fieldState` | `invalid`, `isTouched`, `isDirty`, `error` | Show this field's error and styling | | `formState` | the form's state, e.g. `isSubmitting` | Rarely needed per field | ## Wiring the date-picker, step by step 1. Put the field in `useForm({ defaultValues: { birthDate: null } })`. 2. Render `<Controller name="birthDate" control={control} rules={...} render={...} />`. 3. Map `field.value` to the picker's selected-value prop. 4. Map `field.onChange` to the picker's change callback. `field.onChange` accepts either a DOM event or a plain value, so passing the `Date` directly works. 5. Map `field.onBlur` to the picker's blur callback, so `touchedFields` updates and `onBlur`/`onTouched` validation modes fire. 6. Map `field.ref` to the ref the picker forwards to its focusable element (some libraries name it `inputRef`), so RHF can focus the field when submit fails. 7. Render `fieldState.error?.message` next to it. ## Defaults, null and transforms The documentation is strict about `undefined`: - `undefined` is not a valid default for a `Controller` field; use `null` or an empty string. - If `field.value` is `undefined` on the first render, the widget starts uncontrolled and React warns when a value arrives later. - Calling `field.onChange(undefined)` is invalid; clear to `null` instead. You can **transform** on the way in: `onChange={(d) => field.onChange(d ? toIsoDate(d) : null)}` stores a string while the picker works with `Date` objects. Transform on the way out for display the same way. ## Re-render scope Because the widget is controlled, the `Controller` re-renders on each change of its own field — it subscribes to that field's value internally. The component that called `useForm` does **not** re-render because of it, so a signup form with one controlled date-picker among many registered inputs stays cheap. ## Pitfalls - **Double registration.** Spreading `{...register('birthDate')}` inside the render prop registers the name twice; `Controller` already registered it. - **Dropping `onBlur`.** The value still saves, but the field never becomes touched, so `mode: 'onBlur'` or `'onTouched'` never validates it. - **A ref that goes nowhere.** Focus-on-error works only if the picker forwards the ref to a focusable element. - **`setValue` instead of `field.onChange`.** Inside the render prop, `field.onChange` is the channel that keeps `formState` in step.

  • When would you use useController instead of the Controller component in React Hook Form?
    When you are building a reusable input component. `useController({ name, control, rules })` returns the same `field` and `fieldState` inside your component, so callers write `<DateField name="birthDate" control={control} />` without a render prop. Behaviour is identical; it is the hook `Controller` is built on.
  • How do you store an ISO string in React Hook Form while the date-picker works with Date objects?
    Transform in the render prop: call `field.onChange(date ? date.toISOString().slice(0, 10) : null)` and parse `field.value` back into a `Date` for the picker's value prop. The form data and validation then see the string; only the widget sees `Date` objects.
  • Why does the date-picker not receive focus when a React Hook Form submit fails validation?
    Focus-on-error calls `focus()` on whatever `field.ref` was attached to. If the picker does not forward that ref to a focusable element — or you never passed it — RHF has nothing to focus. Attach `field.ref` to the picker's input ref prop.

saying these in an interview costs you the question

  • Just spread register() onto the date-picker component; it works for any component
  • field.onBlur is optional because the value is saved on change anyway
  • An undefined default is fine for a Controller field
  • Call register inside the render prop so the rules are applied
  • Use setValue in the picker's onChange instead of field.onChange