React Hook Form
Quick Start
Basic Form
import { useForm } from "react-hook-form";
function BasicForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName")} />
<input {...register("lastName", { required: "Last name is required" })} />
{errors.lastName && <p>{errors.lastName.message}</p>}
<input {...register("age", { pattern: /\d+/ })} />
{errors.age && <p>Please enter number for age.</p>}
<input type="submit" />
</form>
);
}
Form with Default Values
import { useForm } from "react-hook-form";
function FormWithDefaults() {
const { register, handleSubmit } = useForm({
defaultValues: {
firstName: "John",
lastName: "Doe",
email: "john@example.com",
},
});
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName")} />
<input {...register("lastName")} />
<input {...register("email")} />
<button type="submit">Submit</button>
</form>
);
}
Common Patterns
Form Validation
import { useForm } from "react-hook-form";
function ValidationExample() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register("username", {
required: "Username is required",
minLength: {
value: 3,
message: "Username must be at least 3 characters",
},
maxLength: {
value: 20,
message: "Username must be less than 20 characters",
},
})}
/>
{errors.username && <span>{errors.username.message}</span>}
<input
{...register("email", {
required: "Email is required",
pattern: {
value: /^\S+@\S+$/i,
message: "Invalid email format",
},
})}
/>
{errors.email && <span>{errors.email.message}</span>}
<button type="submit">Submit</button>
</form>
);
}
Password Confirmation
import { useForm } from "react-hook-form";
function PasswordForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="password"
{...register("password", {
required: "Password is required",
minLength: {
value: 8,
message: "Password must be at least 8 characters",
},
})}
/>
{errors.password && <span>{errors.password.message}</span>}
<input
type="password"
{...register("confirmPassword", {
required: "Please confirm your password",
validate: (value, formValues) =>
value === formValues.password || "Passwords do not match",
})}
/>
{errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}
<button type="submit">Submit</button>
</form>
);
}
Using Controller for Custom Components
import { useForm, Controller } from "react-hook-form";
import Select from "react-select";
function CustomInputForm() {
const { handleSubmit, control } = useForm();
const options = [
{ value: "chocolate", label: "Chocolate" },
{ value: "strawberry", label: "Strawberry" },
{ value: "vanilla", label: "Vanilla" },
];
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="flavor"
control={control}
rules={{ required: "Please select a flavor" }}
render={({ field, fieldState: { error } }) => (
<div>
<Select {...field} options={options} />
{error && <span>{error.message}</span>}
</div>
)}
/>
<button type="submit">Submit</button>
</form>
);
}
Dynamic Fields with useFieldArray
import { useForm, useFieldArray } from "react-hook-form";
function DynamicFieldsForm() {
const { register, handleSubmit, control } = useForm({
defaultValues: {
users: [{ name: "", email: "" }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: "users",
});
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<div key={field.id}>
<input
{...register(`users.${index}.name`, {
required: "Name is required",
})}
placeholder="Name"
/>
<input
{...register(`users.${index}.email`, {
required: "Email is required",
})}
placeholder="Email"
/>
<button type="button" onClick={() => remove(index)}>
Remove
</button>
</div>
))}
<button type="button" onClick={() => append({ name: "", email: "" })}>
Add User
</button>
<button type="submit">Submit</button>
</form>
);
}
Async Form Submission
import { useForm } from "react-hook-form";
function AsyncSubmitForm() {
const {
register,
handleSubmit,
formState: { isSubmitting },
} = useForm();
const onSubmit = async (data) => {
try {
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 2000));
console.log("Form submitted:", data);
} catch (error) {
console.error("Submission failed:", error);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("name", { required: true })} />
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Submitting..." : "Submit"}
</button>
</form>
);
}
Form with Context (FormProvider)
import { useForm, FormProvider, useFormContext } from "react-hook-form";
function NestedInput({ name, label }) {
const {
register,
formState: { errors },
} = useFormContext();
return (
<div>
<label>{label}</label>
<input {...register(name)} />
{errors[name] && <span>{errors[name].message}</span>}
</div>
);
}
function ContextForm() {
const methods = useForm();
const onSubmit = (data) => console.log(data);
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
<NestedInput name="firstName" label="First Name" />
<NestedInput name="lastName" label="Last Name" />
<button type="submit">Submit</button>
</form>
</FormProvider>
);
}
Async Field Validation
import { useForm } from "react-hook-form";
function AsyncValidationForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
const checkUsernameAvailability = async (username) => {
// Simulate API call to check username
await new Promise((resolve) => setTimeout(resolve, 500));
const takenUsernames = ["admin", "user", "test"];
if (takenUsernames.includes(username.toLowerCase())) {
return "Username is already taken";
}
return true;
};
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register("username", {
required: "Username is required",
validate: checkUsernameAvailability,
})}
/>
{errors.username && <span>{errors.username.message}</span>}
<button type="submit">Submit</button>
</form>
);
}
Key Methods
useForm() Hook
const {
register, // Register input fields
handleSubmit, // Form submission handler
formState, // Form state (errors, dirty, isValid, etc.)
setValue, // Set field value programmatically
getValues, // Get form values
reset, // Reset form to default values
trigger, // Trigger validation
clearErrors, // Clear specific or all errors
setError, // Set custom errors
control, // Control object for Controller
} = useForm();
Form State
const {
errors, // Validation errors
dirty, // Form is dirty (has unsaved changes)
dirtyFields, // Array of dirty field names
isDirty, // Boolean indicating if form is dirty
touched, // Array of touched field names
touchedFields, // Object of touched field states
isSubmitted, // Form has been submitted
isSubmitting, // Form is currently submitting
isValid, // Form passes all validations
isValidating, // Form is currently validating
} = formState;
Common Validation Rules
register("fieldName", {
required: "Field is required",
minLength: { value: 3, message: "Minimum 3 characters" },
maxLength: { value: 50, message: "Maximum 50 characters" },
min: { value: 18, message: "Must be at least 18" },
max: { value: 100, message: "Must be less than 100" },
pattern: { value: /^\d+$/, message: "Numbers only" },
validate: (value) => value === "expected" || "Custom validation failed",
});