In React Hook Form, what does register('email', { required: true }) give an input, and what does handleSubmit do before calling your callback?
answer
- four props to spread
- ref reads the DOM value
- rules stored with the field
- preventDefault, validate, then onValid
basics
~20 sIn React Hook Form, register returns name, ref, onChange and onBlur to spread onto a native input. handleSubmit prevents the default submit, validates every field, then calls onValid(data) only when no errors remain, otherwise onInvalid(errors).
solid answer
~40 s`register('email', { required: 'Email is required' })` returns `{ name, ref, onChange, onBlur }`, which you spread onto a native `<input>`. The `ref` lets the library read the value straight from the DOM element and focus it when it is invalid; the rules object is stored with the field. `handleSubmit(onValid, onInvalid?)` builds the submit handler: it calls `preventDefault()`, sets `formState.isSubmitting` to `true`, runs validation (the built-in rules, or the resolver if one is configured), and then calls `onValid(data)` with the collected values when there are no errors — or `onInvalid(errors)` and focuses the first invalid field when there are. Finally it sets `isSubmitting` back to `false`, `isSubmitted` to `true` and increments `submitCount`. Messages are read from `formState.errors.email?.message`.
code
tsx · 40 linesimport { useForm } from 'react-hook-form';
type Contact = { name: string; email: string; seats: number };
export function ContactForm({ save }: { save: (c: Contact) => Promise<void> }) {
const {
register,
handleSubmit,
setError,
formState: { errors, isSubmitting },
} = useForm<Contact>({ defaultValues: { name: '', email: '', seats: 1 } });
const onValid = async (data: Contact) => {
try {
await save(data);
} catch {
setError('root.serverError', { message: 'Could not save, try again' });
}
};
return (
<form onSubmit={handleSubmit(onValid)}>
<input {...register('name', { required: 'Name is required' })} />
{errors.name && <p role="alert">{errors.name.message}</p>}
<input
{...register('email', {
required: 'Email is required',
pattern: { value: /^\S+@\S+$/, message: 'Enter a valid email' },
})}
/>
{errors.email && <p role="alert">{errors.email.message}</p>}
<input type="number" {...register('seats', { valueAsNumber: true, min: 1 })} />
{errors.root?.serverError && <p role="alert">{errors.root.serverError.message}</p>}
<button disabled={isSubmitting}>Send</button>
</form>
);
}go deeper
Be able to write a registered input and a handleSubmit-wrapped form from memory, and name the four props register returns.
Walk through handleSubmit's order: preventDefault, isSubmitting, validation, onValid or onInvalid, then submitCount, and explain why the ref matters.
Show how you surface server failures with setError on a root key, keep isSubmitting honest with an awaited callback, and avoid clobbering register's handlers.
Discuss when uncontrolled registration is the right default for a team's forms and where controlled components force exceptions.
## What register gives an input React Hook Form (RHF) is built around **registration**: instead of holding each field's value in React state, you hand the library a reference to the real DOM input and let the browser keep the value. `register(name, options?)` is the call that does this. It returns a small object: | Property | Type | What it is for | |---|---|---| | `name` | string | The field path, e.g. `email` or `address.city` | | `ref` | ref callback | Connects RHF to the DOM element so it can read the value and focus it | | `onChange` | handler | Tells RHF the value changed, so it can update its store and maybe validate | | `onBlur` | handler | Tells RHF the field was left, which marks it touched and may trigger validation | The idiomatic use is to spread it: `<input {...register('email')} />`. Because the input stays **uncontrolled** — there is no `value` prop — typing does not have to re-render the component that called `useForm`. The `name` is a **path**: `register('address.city')` produces `{ address: { city: ... } }` in the submitted data, and `register('tags.0')` produces an array entry. ## Built-in rules The second argument holds validation rules that live with the field: - `required` — a boolean or a message string, e.g. `required: 'Email is required'` - `min` / `max` — numeric bounds - `minLength` / `maxLength` — string length bounds - `pattern` — a regular expression, optionally `{ value, message }` - `validate` — a function (or an object of named functions) returning `true`, a message string, or `false` - `valueAsNumber` / `valueAsDate` — convert the string the input holds before validation runs Each rule accepts either a bare value or `{ value, message }`. Native inputs always hold strings, so a `type="number"` field reaches your data as a string unless you add `valueAsNumber: true`. ## What handleSubmit does, in order `handleSubmit(onValid, onInvalid?)` does not submit anything itself; it **returns** an async event handler you pass to `<form onSubmit>`. When the form is submitted, that handler: 1. Calls `event.preventDefault()`, so the page does not reload. 2. Sets `formState.isSubmitting` to `true`. 3. Validates: runs the resolver if `useForm` was given one, otherwise every registered field's built-in rules. 4. If there are **no errors**, awaits `onValid(data, event)` with the collected values. 5. If there **are** errors, awaits `onInvalid(errors, event)` when you supplied it, and focuses the first invalid field (the `shouldFocusError` option, `true` by default). 6. Sets `isSubmitting` to `false`, `isSubmitted` to `true`, `isSubmitSuccessful`, and increments `submitCount`. Two details matter in production. First, because step 4 is **awaited**, `isSubmitting` stays `true` for as long as an async `onValid` is waiting on a request, which is exactly what you want for disabling a button. Second, `handleSubmit` **does not swallow** an error thrown by `onValid`: it records `isSubmitSuccessful: false`, finishes the state update, then rethrows. The documented pattern is a `try/catch` inside `onValid` that calls `setError('root.serverError', { message })` for a server-side failure. Inputs registered with `disabled: true` are left out of the submitted data (their value is `undefined`). ## Reading errors Errors live in `formState.errors`, shaped like your values: `errors.email?.message`, `errors.address?.city?.message`. Each entry also carries a `type` such as `'required'` or `'pattern'`, which lets you pick a message per rule. ```tsx const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<{ email: string }>(); <form onSubmit={handleSubmit(save)}> <input {...register('email', { required: 'Email is required' })} /> {errors.email && <p role="alert">{errors.email.message}</p>} <button disabled={isSubmitting}>Subscribe</button> </form> ``` ## Common mistakes - **Adding your own `onChange` after the spread.** A later `onChange` prop replaces the one `register` returned, so RHF stops hearing changes. Pass it as the `onChange` option of `register` instead. - **Calling `preventDefault()` inside `onValid`.** It is harmless but shows a misunderstanding: `handleSubmit` already did it before validating. - **Expecting `onValid` to run with errors.** It runs only when validation passed; failures go to `onInvalid`. - **Registering the same input twice**, for example with `register` and a `Controller`, which makes two registrations fight over one name.
- How do you run your own change handler on a registered input without breaking React Hook Form?Pass it as the `onChange` option of `register`, for example `register('email', { onChange: (e) => track(e) })`. Spreading `register(...)` and then writing an `onChange` prop after it replaces the library's handler, so RHF stops seeing the input change and its value, dirty state and validation go stale.
- What happens if the onValid callback passed to handleSubmit throws?`handleSubmit` does not swallow it. It still finishes its state update — `isSubmitting` back to `false`, `isSubmitted` to `true`, `isSubmitSuccessful` to `false`, `submitCount` incremented — and then rethrows the error. The recommended pattern is a `try/catch` inside `onValid` that calls `setError('root.serverError', ...)` so the failure shows in the form.
- Why does a type="number" input registered with React Hook Form submit a string?A native input's value is always a string, and `register` reads it from the DOM as-is. Add `valueAsNumber: true` to the register options and RHF converts it before validation runs, returning `NaN` when the text is not a number. `valueAsDate` does the same for dates.
saying these in an interview costs you the question
- register returns a value and setter pair, like useState
- handleSubmit calls onValid even when validation fails, passing the errors too
- You must call e.preventDefault() yourself inside the onValid callback
- A type="number" input arrives in the data as a number automatically
- Writing onChange after {...register('x')} is a safe way to add behaviour