XGitHub

React Hook Form Integration

Learn how to use NumoraInput with react-hook-form using the recommended Controller pattern.

Overview

NumoraInput manages its own DOM value directly (via an uncontrolled defaultValue internally), but supports a controlled-style value prop that syncs programmatic changes into the input. React Hook Form's Controller component is the recommended integration pattern - it handles the value, onChange, onBlur, ref, and disabled wiring automatically.

Note: numora-react does not require react-hook-form as a dependency. It works with react-hook-form when it's present in your project.

Controller Pattern (recommended)

Always forward all four field properties - onChange, onBlur, ref, and disabled. Omitting onBlur breaks touched-state tracking; omitting ref breaks auto-focus on validation errors.

Basic form with Controller pattern - works with setValue() automatically

tsx
import { useForm, Controller } from 'react-hook-form'
import { NumoraInput } from 'numora-react'

function Form() {
  const { control, handleSubmit, setValue } = useForm()

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <Controller
        control={control}
        name="amount"
        render={({ field: { onChange, onBlur, value, ref, disabled } }) => (
          <NumoraInput
            ref={ref}
            name="amount"
            value={value || ''}
            onChange={onChange}
            onBlur={onBlur}
            disabled={disabled}
            maxDecimals={2}
            thousandSeparator=","
          />
        )}
      />
      <button type="button" onClick={() => setValue('amount', '1000')}>
        Set to 1000
      </button>
      <button type="submit">Submit</button>
    </form>
  )
}

Validation errors

Use fieldState.error from the render prop to display validation messages:

tsx
<Controller
  control={control}
  name="amount"
  rules={{ required: 'Amount is required', min: { value: 1, message: 'Must be at least 1' } }}
  render={({ field: { onChange, onBlur, value, ref, disabled }, fieldState: { error } }) => (
    <>
      <NumoraInput
        ref={ref}
        value={value || ''}
        onChange={onChange}
        onBlur={onBlur}
        disabled={disabled}
        maxDecimals={2}
        thousandSeparator=","
      />
      {error && <span>{error.message}</span>}
    </>
  )}
/>

💡 Tip: NumoraInput's onChange always exposes the raw (unformatted) numeric string - no post-processing required:

  • e.target.value - raw numeric string (e.g. "1000.50") - separators stripped
  • e.target.formattedValue - formatted display string (e.g. "1,000.50"), typed via NumoraHTMLInputElement

Complete example

A form with programmatic updates (Max / Half buttons) using the Controller pattern:

tsx
import { useForm, Controller } from 'react-hook-form'
import { NumoraInput } from 'numora-react'

interface FormValues {
  amount: string
}

function SwapForm() {
  const { control, handleSubmit, setValue, watch } = useForm<FormValues>({
    defaultValues: { amount: '' }
  })

  const handleMaxClick = () => {
    setValue('amount', '1000000')
  }

  const handleHalfClick = () => {
    const current = parseFloat(watch('amount') || '0')
    setValue('amount', (current / 2).toString())
  }

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <Controller
        control={control}
        name="amount"
        rules={{ required: 'Amount is required' }}
        render={({ field: { onChange, onBlur, value, ref, disabled }, fieldState: { error } }) => (
          <>
            <NumoraInput
              ref={ref}
              name="amount"
              value={value || ''}
              onChange={onChange}
              onBlur={onBlur}
              disabled={disabled}
              maxDecimals={6}
              thousandSeparator=","
              placeholder="0.0"
            />
            {error && <span>{error.message}</span>}
          </>
        )}
      />
      <div>
        <button type="button" onClick={handleMaxClick}>Max</button>
        <button type="button" onClick={handleHalfClick}>Half</button>
      </div>
      <button type="submit">Submit</button>
    </form>
  )
}

Register pattern (uncontrolled)

For basic forms that don't need programmatic updates or setValue(), you can use the register pattern. Spread the register result and leave NumoraInput to manage its own value - do not also pass a value prop:

tsx
import { useForm } from 'react-hook-form'
import { NumoraInput } from 'numora-react'

function Form() {
  const { register, handleSubmit } = useForm()

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <NumoraInput
        {...register('amount')}
        maxDecimals={2}
        thousandSeparator=","
      />
      <button type="submit">Submit</button>
    </form>
  )
}

⚠️ Limitation: With the register pattern, calling setValue() won't update the displayed value. Use the Controller pattern whenever you need programmatic updates.

Key points

  • Always forward onBlur, ref, and disabled from the Controller field - these are required for touched-state tracking, focus management on errors, and disabled-field support.
  • Controller pattern (recommended): works with setValue(), validation, and all react-hook-form features.
  • Register pattern: works for basic form submission only - setValue() won't update the UI.
  • Raw values by default: e.target.value in onChange always returns the raw numeric string (separators stripped). field.onChange(e.target.value) stores the clean value - no extra handling needed. The formatted display string is available as e.target.formattedValue via NumoraHTMLInputElement.
  • No extra dependencies: numora-react doesn't require react-hook-form.