React Hook Form 04 📋 Registering Inputs — The register function & native HTML inputs
The register function is the bridge between a native HTML input and the form. It returns a set of props—name, ref, onChange, and onBlur—that the input spreads, and the spread is what connects the DOM element to the form’s internal state . The input stays uncontrolled: the value lives in the DOM, and the library reads it through the ref when the form is submitted or when a validation runs. The register call is the default path for every native <input>, <select>, and <textarea>, and it is the fast path because typing does not re-render the component that owns the field.
The function accepts a name and an optional options object. The name is the field’s path in the form values, and the path can be nested with the dot syntax. The options are the validation rules and the value transformations: the required, the min/max, the minLength/maxLength, the pattern, the validate function, the valueAsNumber, the valueAsDate, and the setValueAs . The rules are based on the HTML standard, and the custom validate function is the escape hatch for the rules that the standard does not cover.
Key point: register(name, options) returns the props that connect a native input to the form. The input stays uncontrolled, and typing does not re-render the form. The name supports the dot syntax for the nested fields, and the options hold the validation rules and the value transformations. The valueAsNumber and the valueAsDate convert the text value before the validation runs, and the validate function handles the custom rules.
Why the register function is the default
The uncontrolled input problem. A native input stores its value in the DOM, and React does not need to know the value until the form is submitted. The register function connects the input to the form through a ref, and the library reads the value from the DOM when it needs it . The uncontrolled model is the reason the typing does not trigger a re-render, and the re-render is the cost that the register avoids.
The validation problem. The native inputs have the built-in validation attributes—the required, the min, the max, the pattern—and the browser can enforce them. The register options mirror these attributes, but they run through the library’s validation instead of the browser’s . The advantage is that the errors are the React state, and the messages are the custom strings. The progressive option is the mechanism that forwards the rules as the native attributes, which lets the browser enforce them before the JavaScript loads .
The value transformation problem. A native input always returns a string, and a numeric or a date field needs the conversion. The valueAsNumber converts the string to a number, and the valueAsDate converts it to a date. The conversion runs before the validation, and the setValueAs is the custom function for the fields that need a different transformation . The transformation is the mechanism that keeps the form values typed without the manual parseInt in the submit handler.
The ref forwarding problem. The register only works with the components that forward a ref to a native input. A third-party select, a date picker, or a masked input that does not expose a native ref cannot use the register, and the Controller is the escape hatch . The rule is simple: if the component forwards the ref to the native input, the register works; if it only accepts the value and the onChange, the Controller is required.
The field name problem. The name is the field’s path in the form values, and the path supports the dot syntax for the nested fields. The register("address.city") produces the { address: { city } } object, and the register("tags.0") produces the array. The bracket syntax is not supported for the TypeScript consistency, and the name must not start with a number . The naming is the mechanism that maps the DOM input to the form values.
The trade-off. The register is the fast path, but it has the constraints. The input must be a native element, the name must be a valid path, and the value transformation must be declared. The constraints are the cost of the uncontrolled model, and the Controller is the escape hatch for the components that cannot meet them.
a. The basic registration and the field naming
The register call returns the props, and the input spreads them. The name is the field’s path, and the ref is the connection to the DOM element.
// The basic registration
import { useForm } from "react-hook-form";
function LoginForm() {
const { register, handleSubmit } = useForm({
defaultValues: { email: "", password: "" },
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
{/* The register returns the props. */}
{/* The input stays uncontrolled. */}
<input {...register("email")} type="email" />
<input {...register("password")} type="password" />
<button type="submit">Log in</button>
</form>
);
}
The name supports the dot syntax for the nested fields. The register("address.city") produces the { address: { city } } object, and the register("tags.0") produces the array. The bracket syntax is not supported for the TypeScript consistency, so the register("test.0.firstName") is the correct form and the register("test[0].firstName") is not.
// The nested field names
<input {...register("address.city")} />
<input {...register("address.zip")} />
// The result:
// { address: { city: "...", zip: "..." } }
// The array field names
<input {...register("tags.0")} />
<input {...register("tags.1")} />
// The result:
// { tags: ["...", "..."] }
The name must be unique, except for the native radio and checkbox inputs, where the same name is the mechanism that groups the inputs. The name must not start with a number or use the numbers as the standalone keys, and it should avoid the special characters . The dot syntax is the only supported form for the array and the object values.
The radio group and the checkbox group are the grouped inputs. The same name on the multiple radio inputs is the mechanism that groups them, and the selected value is the one that the form stores. The checkbox with the same name is the array, and the checkbox with the distinct names is the individual booleans.
// The radio group
<input type="radio" value="male" {...register("gender")} />
<input type="radio" value="female" {...register("gender")} />
// The checkbox array
<input type="checkbox" value="react" {...register("skills")} />
<input type="checkbox" value="vue" {...register("skills")} />
// The individual checkbox
<input type="checkbox" {...register("subscribe")} />
b. The validation rules and the value transformations
The register options are the validation rules and the value transformations. The rules are the required, the min, the max, the minLength, the maxLength, the pattern, and the validate function. The transformations are the valueAsNumber, the valueAsDate, and the setValueAs.
// The validation rules
<input
{...register("email", {
// The required rule with the custom message.
required: "The email is required.",
// The pattern rule with the custom message.
pattern: {
value: /^\S+@\S+$/i,
message: "The email is invalid.",
},
})}
type="email"
/>
// The length rules
<input
{...register("username", {
minLength: { value: 3, message: "At least 3 characters." },
maxLength: { value: 20, message: "At most 20 characters." },
})}
/>
The required rule accepts a boolean or a string. The string is the error message, and the boolean uses the default message. The minLength and the maxLength accept a number or an object with the value and the message. The min and the max are the numeric equivalents for the number inputs . The pattern accepts the RegExp or an object with the value and the message.
The validate function is the custom rule. The function receives the field value and, since the v7.42.0, the full form values as the second argument. It returns true or undefined for the valid field, a string for the invalid field with the message, or false for the invalid field with no custom message . The validate is the mechanism for the rules that the built-in rules cannot express, such as the password confirmation or the username availability.
// The custom validate function
<input
{...register("username", {
validate: async (value) => {
// The function can be async.
const isFree = await checkUsername(value);
return isFree || "This username is taken.";
},
})}
/>
// The password confirmation
<input
{...register("passwordConfirm", {
validate: (value, formValues) =>
value === formValues.password || "The passwords do not match.",
})}
/>
The valueAsNumber and the valueAsDate are the transformations. The valueAsNumber converts the input string to a number, and the empty field becomes NaN. The valueAsDate converts the string to a date, and the empty field becomes an Invalid Date. The conversion runs before the validation, and it is not applied to the defaultValues . The setValueAs is the custom function for the fields that need a different transformation, such as the trimmed string or the parsed object.
// The valueAsNumber
<input type="number" {...register("age", { valueAsNumber: true })} />
// The valueAsDate
<input type="date" {...register("birthdate", { valueAsDate: true })} />
// The setValueAs
<input
{...register("price", {
setValueAs: (value) => parseFloat(value) || 0,
})}
/>
The disabled option is the mechanism that makes the field return undefined and the input disabled. The disabled prop also omits the built-in validation rules, which is the behavior that the schema validation can leverage . The shouldUnregister option is the mechanism that removes the field’s value when the input unmounts, and it should be avoided with the useFieldArray.
The deps option is the mechanism that re-validates the dependent fields. The register("passwordConfirm", { deps: ["password"] }) re-validates the confirmation when the password changes, and the re-validation is the mechanism that keeps the two fields in sync.
// The dependent field
<input
{...register("passwordConfirm", {
deps: ["password"],
validate: (value, formValues) =>
value === formValues.password || "The passwords do not match.",
})}
/>
Complete Example Session
// ============================================
// PART 1: THE BASIC REGISTRATION
// ============================================
import { useForm } from "react-hook-form";
function LoginForm() {
const { register, handleSubmit } = useForm({
defaultValues: { email: "", password: "" },
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("email")} type="email" />
<input {...register("password")} type="password" />
<button type="submit">Log in</button>
</form>
);
}
// ============================================
// PART 2: THE NESTED FIELD
// ============================================
function AddressForm() {
const { register, handleSubmit } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
{/* The dot syntax is the nested field. */}
<input {...register("address.city")} />
<input {...register("address.zip")} />
{/* The result: { address: { city, zip } } */}
</form>
);
}
// ============================================
// PART 3: THE ARRAY FIELD
// ============================================
function TagsForm() {
const { register, handleSubmit } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
{/* The dot syntax is the array index. */}
<input {...register("tags.0")} />
<input {...register("tags.1")} />
{/* The result: { tags: [ "...", "..." ] } */}
</form>
);
}
// ============================================
// PART 4: THE VALIDATION RULES
// ============================================
function SignUpForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input
{...register("email", {
required: "The email is required.",
pattern: {
value: /^\S+@\S+$/i,
message: "The email is invalid.",
},
})}
/>
{errors.email && <p>{errors.email.message}</p>}
</form>
);
}
// ============================================
// PART 5: THE CUSTOM VALIDATE
// ============================================
function UsernameForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input
{...register("username", {
validate: async (value) => {
const isFree = await checkUsername(value);
return isFree || "This username is taken.";
},
})}
/>
{errors.username && <p>{errors.username.message}</p>}
</form>
);
}
// ============================================
// PART 6: THE VALUE TRANSFORMATION
// ============================================
function AgeForm() {
const { register, handleSubmit } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
{/* The valueAsNumber converts the string to a number. */}
<input type="number" {...register("age", { valueAsNumber: true })} />
{/* The valueAsDate converts the string to a date. */}
<input type="date" {...register("birthdate", { valueAsDate: true })} />
</form>
);
}
// ============================================
// PART 7: THE PASSWORD CONFIRMATION
// ============================================
function PasswordForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("password")} type="password" />
<input
{...register("passwordConfirm", {
deps: ["password"],
validate: (value, formValues) =>
value === formValues.password || "The passwords do not match.",
})}
type="password"
/>
{errors.passwordConfirm && <p>{errors.passwordConfirm.message}</p>}
</form>
);
}
// ============================================
// PART 8: THE DISABLED FIELD
// ============================================
function DisabledForm() {
const { register, handleSubmit } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
{/* The disabled field returns undefined. */}
<input {...register("coupon", { disabled: true })} />
</form>
);
}
// ============================================
// PART 9: THE CONTROLLED COMPONENT
// ============================================
import { Controller } from "react-hook-form";
function ControlledForm() {
const { control, handleSubmit } = useForm();
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
{/* The Controller is for the components without the native ref. */}
<Controller
name="date"
control={control}
render={({ field }) => <DatePicker {...field} />}
/>
</form>
);
}
// ============================================
// PART 10: THE CHECKLIST
// ============================================
// [ ] The register is used for the native inputs.
// [ ] The name uses the dot syntax for the nested fields.
// [ ] The validation rules are set in the register options.
// [ ] The validate function is used for the custom rules.
// [ ] The valueAsNumber and valueAsDate are used for the numeric and date fields.
// [ ] The Controller is used for the controlled components.
// [ ] The disabled option is used for the fields that should not submit.
// [ ] The deps option is used for the dependent fields.
The ten parts covered the basic registration, the nested field, the array field, the validation rules, the custom validate, the value transformation, the password confirmation, the disabled field, the controlled component, and the checklist. The through-line is that the register is the connection to the native input, and the options are the validation and the transformation.
Quick Reference
The register signature
| Parameter | Type | Purpose |
|---|---|---|
name | string | The field path |
options | RegisterOptions | The validation and the transformation |
The validation rules
| Rule | Type | Purpose |
|---|---|---|
required | boolean | string | The field is required |
min / max | number | { value, message } | The numeric range |
minLength / maxLength | number | { value, message } | The string length |
pattern | RegExp | { value, message } | The regex pattern |
validate | function | Record<string, function> | The custom validation |
deps | string | string[] | The dependent fields |
The value transformations
| Option | Type | Purpose |
|---|---|---|
valueAsNumber | boolean | Convert to a number |
valueAsDate | boolean | Convert to a date |
setValueAs | function | The custom conversion |
The field naming
| Syntax | Result |
|---|---|
register("email") | { email: value } |
register("address.city") | { address: { city: value } } |
register("tags.0") | { tags: [value] } |
Best Practices
✅ Do This:
// Use the register for the native inputs
<input {...register("email")} /> // ✅
// Use the dot syntax for the nested fields
<input {...register("address.city")} /> // ✅
// Use the valueAsNumber for the numeric fields
<input type="number" {...register("age", { valueAsNumber: true })} /> // ✅
// Use the validate for the custom rules
register("username", { validate: (v) => v.length > 3 || "Too short" }) // ✅
❌ Don’t Do This:
// Use the bracket syntax for the array fields
register("tags[0]") // the TypeScript inconsistency // ❌
// Use the register for the controlled components
register("date") // the DatePicker has no native ref // ❌
// Forget the valueAsNumber for the numeric field
register("age") // the value is a string // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The numeric field is a string | The valueAsNumber is missing | Add valueAsNumber: true |
| The nested field is not tracked | The bracket syntax is used | Use the dot syntax |
| The controlled component is not registered | The register is used without a native ref | Use the Controller |
| The custom validation does not run | The validate is in the wrong options | Put it in the register options |
| The dependent field does not re-validate | The deps is missing | Add the deps option |
Real-World Examples
1. The basic register
<input {...register("email")} />
2. The nested register
<input {...register("address.city")} />
3. The array register
<input {...register("tags.0")} />
4. The required rule
register("email", { required: "Required" })
5. The pattern rule
register("email", { pattern: /^\S+@\S+$/i })
6. The custom validate
register("username", { validate: async (v) => await check(v) })
7. The valueAsNumber
register("age", { valueAsNumber: true })
8. The valueAsDate
register("birthdate", { valueAsDate: true })
9. The deps
register("passwordConfirm", { deps: ["password"] })
10. The disabled field
register("coupon", { disabled: true })
Visual
The register Props
┌──────────────────────────────────────────────────────────────┐
│ THE REGISTER RETURN VALUE │
│ │
│ register("email") │
│ ┌─────────────────────┐ │
│ │ name: "email" │ The field path │
│ │ ref: callback │ The DOM connection │
│ │ onChange: fn │ The change handler │
│ │ onBlur: fn │ The blur handler │
│ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ <input {...props} />│ The spread connects the input │
│ └─────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
The Field Naming
┌──────────────────────────────────────────────────────────────┐
│ THE DOT SYNTAX │
│ │
│ register("email") → { email: value } │
│ register("address.city")→ { address: { city: value } } │
│ register("tags.0") → { tags: [value] } │
│ │
│ The dot syntax is the only supported form. │
│ The bracket syntax is not supported. │
└──────────────────────────────────────────────────────────────┘
The Value Transformation
┌──────────────────────────────────────────────────────────────┐
│ THE TRANSFORMATION FLOW │
│ │
│ The user types "42" │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ valueAsNumber: true │ The string is converted │
│ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ The value is 42 │ The number, not the string │
│ └──────────┬──────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ The validation runs │ The min/max rules work │
│ └─────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
register | Connects the native input to the form |
| Return value | name, ref, onChange, onBlur |
| Field naming | The dot syntax for the nested and array fields |
| Validation rules | required, min, max, minLength, maxLength, pattern, validate |
| Value transformations | valueAsNumber, valueAsDate, setValueAs |
| Dependent fields | The deps option |
| Controlled components | The Controller is the escape hatch |
Key takeaways:
- The
registerconnects the native input to the form. It returns thename, theref, theonChange, and theonBlurprops, and the input spreads them. The input stays uncontrolled, and typing does not re-render the component. - The
namesupports the dot syntax for the nested fields. Theregister("address.city")produces the nested object, and theregister("tags.0")produces the array. The bracket syntax is not supported for the TypeScript consistency. - The validation rules are the HTML standard plus the custom
validate. Therequired, themin, themax, theminLength, themaxLength, and thepatternare the built-in rules, and thevalidatefunction is the custom rule that returns the error message. - The value transformations convert the string before the validation. The
valueAsNumberand thevalueAsDateare the built-in conversions, and thesetValueAsis the custom function. The conversion is the mechanism that keeps the form values typed without the manual parsing. - The
registeronly works with the native inputs. The controlled components that only accept thevalueand theonChangeneed theController, and the rule is the ref forwarding: if the component forwards the ref to a native input, theregisterworks; otherwise, theControlleris required. - The
depsoption is the dependent-field mechanism. Theregister("passwordConfirm", { deps: ["password"] })re-validates the confirmation when the password changes.
Remember: The register function is the connection between the native input and the form. It returns the props that the input spreads, and the input stays uncontrolled. The name is the field path with the dot syntax, and the options are the validation rules and the value transformations. The register is the fast path, and the Controller is the escape hatch for the controlled components. The rules are the HTML standard, and the validate function is the custom escape. The deps option keeps the dependent fields in sync.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!