reset() with no arguments to put every field back to its defaultValues. Call reset(values) to load new values, which also become the new defaults. To clear a form after a successful submit, run reset() in a useEffect that watches formState.isSubmitSuccessful, not inside onSubmit. Use resetField(name) for a single field. Most "reset not working" bugs come from a missing or partial defaultValues, or a UI component wired with defaultValue instead of value.Point any HTML form at one endpoint. 500 free submissions — no card, no time limit — and unlimited forms.
Create free accountThe short version
This is the whole pattern for a form that clears itself after a successful submit. The two parts that matter are complete defaultValues (every field, empty strings rather than undefined) and the effect at the bottom.
"use client";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
type ContactValues = { name: string; email: string; message: string };
export default function ContactForm() {
const {
register,
handleSubmit,
reset,
formState: { isSubmitSuccessful },
} = useForm<ContactValues>({
defaultValues: { name: "", email: "", message: "" },
});
// Reset after handleSubmit has finished, not inside onSubmit.
useEffect(() => {
if (isSubmitSuccessful) reset();
}, [isSubmitSuccessful, reset]);
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("name")} />
<input type="email" {...register("email")} />
<textarea {...register("message")} />
<button type="submit">Send</button>
</form>
);
}Everything below is tested against react-hook-form 7.89, the current v7 release. The API has been stable across v7, but a few options are newer: keepDirtyValues arrived in 7.31, the values prop in 7.41 and keepIsSubmitSuccessful in 7.47.
What reset() actually resets
The name undersells it. reset(values?, options?) rebuilds the form's state from scratch. With no options it:
- sets every field's value (to
valuesif you passed them, otherwise to the stored defaults), - replaces the stored
defaultValueswith whatever you passed, - clears
errors,dirtyFieldsandtouchedFields, - sets
isSubmittedandisSubmitSuccessfulback tofalseandsubmitCountback to 0.
The second point is the one that surprises people. reset(data) does not just fill the inputs: data becomes the baseline that isDirty compares against, and the target of every later no-argument reset(). That is exactly what you want after loading or saving a record, and exactly what you don't want if you meant "fill these fields but keep the original defaults". For that, pass keepDefaultValues: true.
You can also pass a function. reset((current) => ...) receives the current form values and returns the next ones, which saves a getValues() call when you only want to clear part of the form.
reset() options cheat sheet
Each keep* option opts one piece of state out of the reset:
// Back to the current defaultValues. Clears errors, dirty, touched, submit state.
reset();
// New values AND new defaultValues (isDirty is false afterwards).
reset({ name: "Ada", email: "[email protected]", message: "" });
// Derive the next values from what is in the form right now.
reset((current) => ({ ...current, message: "" }));
// Keep what the user typed; clear errors, touched and submit state.
reset(undefined, { keepValues: true });
// Load values but keep the ORIGINAL defaults as the dirty baseline.
reset(saved, { keepDefaultValues: true });
// Refresh from the server without overwriting fields the user is editing.
reset(saved, { keepDirtyValues: true });
// Clear values but keep validation messages on screen.
reset(undefined, { keepErrors: true });keepErrors: keep the current error messages.keepDirty: keepdirtyFieldsand the currentisDirty(the flags, not the input values).keepDirtyValues: keep the values of fields the user has changed and update only the untouched ones. Built for refreshing server data under someone who is mid-edit.keepValues: leave the inputs exactly as they are.keepDefaultValues: keep the originaldefaultValues, soisDirtykeeps comparing against them.keepIsSubmitted,keepIsSubmitSuccessful,keepSubmitCount: keep the submit flags.keepTouched,keepIsValid,keepIsValidating: keep touched state, the currentisValid, and in-flight validation state.keepFieldsRef(7.60+): keep the registered input refs instead of re-registering every field.
Reset after submit: the right way
The tempting version is to call reset() at the end of onSubmit. The values do clear, but handleSubmit is not finished when your callback returns. It then sets isSubmitted: true, isSubmitSuccessful: true and bumps submitCount on top of your fresh form. Your "clean" form now believes it has already been submitted, so it validates on every keystroke (reValidateMode) and shows errors before the next visitor has finished typing.
The React Hook Form docs recommend resetting in a useEffect keyed on isSubmitSuccessful, because the effect runs after handleSubmit has written its final state. The other half of the pattern is failure handling. isSubmitSuccessful is only true when onSubmit didn't throw and the form has no errors. So on a failed request, call setError("root.server", ...) and return. The flag stays false, the effect doesn't fire, and the visitor keeps what they typed. If you swallow the failure, the effect clears the form and the message is gone.
Here is the complete component, posting to a hosted form endpoint so there's no API route to write. It sends JSON to https://splitforms.com/api/submit with your access_key (a public form ID, safe in client code), and the endpoint answers { "success": true } or { "success": false, "message": "..." }:
"use client";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
const ACCESS_KEY = "YOUR_ACCESS_KEY"; // public form ID from your splitforms dashboard
type ContactValues = { name: string; email: string; message: string };
type SubmitResponse = { success: boolean; message?: string };
const EMPTY: ContactValues = { name: "", email: "", message: "" };
export default function ContactForm() {
const [sent, setSent] = useState(false);
const {
register,
handleSubmit,
reset,
setError,
formState: { errors, isSubmitting, isSubmitSuccessful },
} = useForm<ContactValues>({ defaultValues: EMPTY });
async function onSubmit(data: ContactValues) {
setSent(false);
try {
const res = await fetch("https://splitforms.com/api/submit", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ access_key: ACCESS_KEY, ...data }),
});
const json = (await res.json()) as SubmitResponse;
if (!res.ok || !json.success) {
// A root error makes isSubmitSuccessful false, so the effect below
// does NOT wipe the user's input.
setError("root.server", {
message: json.message ?? "Something went wrong. Please try again.",
});
return;
}
setSent(true);
} catch {
setError("root.server", { message: "Network error. Check your connection." });
}
}
useEffect(() => {
if (isSubmitSuccessful) reset(EMPTY);
}, [isSubmitSuccessful, reset]);
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<label>
Name
<input {...register("name", { required: "Name is required" })} />
</label>
{errors.name && <p role="alert">{errors.name.message}</p>}
<label>
Email
<input type="email" {...register("email", { required: "Email is required" })} />
</label>
{errors.email && <p role="alert">{errors.email.message}</p>}
<label>
Message
<textarea {...register("message", { required: "Say something" })} />
</label>
{errors.message && <p role="alert">{errors.message.message}</p>}
{errors.root?.server && <p role="alert">{errors.root.server.message}</p>}
{sent && <p role="status">Thanks! Your message is on its way.</p>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Sending..." : "Send"}
</button>
</form>
);
}Why a separate sent state for the thank-you message? Because reset() sets isSubmitSuccessful back to false, you can't render the message from it. You could pass keepIsSubmitSuccessful: true, but then the flag stays true between submissions, the effect's dependency never changes, and the second successful submit doesn't reset the form. A local boolean avoids the trap. If you're validating with a schema, the same component works with a Zod resolver; see the React Hook Form + Zod validation guide. For the version without React Hook Form, see how to add a contact form to React.
resetField(): reset one field
resetField(name, options?) puts a single registered field back to its default and clears that field's error, dirty and touched state, then re-evaluates isDirty and isValid for the form. Use it for "remove coupon", "clear search" and similar per-field buttons.
import { useForm } from "react-hook-form";
type CheckoutValues = { email: string; coupon: string };
export function Checkout() {
const { register, resetField } = useForm<CheckoutValues>({
defaultValues: { email: "", coupon: "" },
});
return (
<form>
<input {...register("email")} />
<input {...register("coupon")} />
{/* Back to its default; clears this field's error, dirty and touched state. */}
<button type="button" onClick={() => resetField("coupon")}>
Remove coupon
</button>
{/* Same, but leave the error message showing. Note: keepError, singular. */}
<button type="button" onClick={() => resetField("coupon", { keepError: true })}>
Clear
</button>
{/* Set a new value AND make it the field's new default. */}
<button
type="button"
onClick={() => resetField("coupon", { defaultValue: "WELCOME10" })}
>
Apply welcome code
</button>
</form>
);
}Three rules from the docs that trip people up:
- The option is
keepError(singular), whileresetuseskeepErrors. TypeScript catches this; plain JavaScript silently ignores it. - The field must be registered (mounted).
resetFieldon a name that isn't registered does nothing. defaultValuecan't beundefined. Passing it sets the value and updates that field's stored default.
Edit forms: repopulating from an API
An edit form has to wait for data, fill itself, and treat the loaded record as "not dirty". React Hook Form gives you two built-in ways to do that, both shown here:
"use client";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
type Profile = { displayName: string; email: string; company: string };
// Option 1: the `values` prop. The form resets itself whenever the object changes.
export function EditProfileForm({ userId }: { userId: string }) {
const [profile, setProfile] = useState<Profile>();
useEffect(() => {
fetch(`/api/profile/${userId}`)
.then((r) => r.json() as Promise<Profile>)
.then(setProfile);
}, [userId]);
const { register, handleSubmit, reset, formState: { isDirty } } = useForm<Profile>({
defaultValues: { displayName: "", email: "", company: "" },
values: profile, // undefined until loaded, then the form fills in
resetOptions: { keepDirtyValues: true }, // a later refresh won't clobber edits
});
async function onSubmit(data: Profile) {
const res = await fetch(`/api/profile/${userId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (res.ok) {
const saved = (await res.json()) as Profile;
// Saved values become the new baseline (isDirty -> false). Override the
// resetOptions default here: it applies to every reset() call, not just
// the automatic one.
reset(saved, { keepDirtyValues: false });
}
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("displayName")} />
<input type="email" {...register("email")} />
<input {...register("company")} />
<button type="submit" disabled={!isDirty}>Save</button>
</form>
);
}
// Option 2: async defaultValues. Loaded once, on mount.
export function EditProfileOnce({ userId }: { userId: string }) {
const { register, formState: { isLoading } } = useForm<Profile>({
defaultValues: async () => {
const res = await fetch(`/api/profile/${userId}`);
return (await res.json()) as Profile;
},
});
if (isLoading) return <p>Loading...</p>;
return (
<form>
<input {...register("displayName")} />
<input type="email" {...register("email")} />
<input {...register("company")} />
</form>
);
}- The
valuesprop is reactive. Whenever the object changes (it's compared deeply, so a re-render with equal data does nothing), the form callsresetinternally with yourresetOptions. It fits data that can change underneath the form, such as a query cache that refetches on window focus. - Async
defaultValuesloads once.formState.isLoadingis true until the promise resolves. The docs note thatdefaultValuesare cached, so later changes needreset. reset(data)in your ownuseEffectafter a fetch still works and is what older tutorials show. Just never callresetduring render; that loops.
One detail we confirmed in the library source and in a test: resetOptions are merged into every reset() call, not only the automatic one. With keepDirtyValues: true set globally, a plain reset(saved) after saving leaves stale entries in dirtyFields even though isDirty reads false. That's why the save handler passes { keepDirtyValues: false } explicitly.
reset() vs setValue()
setValue(name, value) changes one field the way a user would, and by default skips validation, dirty and touched tracking unless you pass shouldValidate, shouldDirty or shouldTouch. It never touches defaultValues. reset replaces the whole form and its baseline. Rule of thumb: setValue for reacting to input (auto-filling a city from a zip code, covered in React Hook Form watch and setValue), reset for loading, saving and clearing.
If you only want to move the baseline, so that the current values count as "saved" without touching the inputs, recent v7 releases (7.77+) add resetDefaultValues(values). It recomputes isDirty and dirtyFields against the new defaults and leaves typed values alone.
reset() not working? The usual causes
1. You passed {} or a partial object
reset({}) is not "clear everything". It replaces your stored defaults with an empty object, and fields with no value in it keep whatever is in the DOM. We ran it against 7.89: a native input the user had typed into still showed the text afterwards. A partial object such as reset({ name: "" }) behaves the same way for every field you left out. Call reset() with no arguments, or pass a complete object with an empty string for each field.
2. A default is undefined
The docs warn against undefined as a default value because it conflicts with a controlled component's default state. A component that starts with value={undefined} is uncontrolled as far as React is concerned, so "resetting" it to undefined changes nothing on screen. Use "", false, [] or null to match the input type.
3. A shadcn or MUI Select uses defaultValue
This is the most common one with component libraries. If the component reads defaultValue={field.value}, it takes the value once on mount and ignores every later change, including reset(). React Hook Form's state is reset correctly; the screen just doesn't show it. Wire it with value:
"use client";
import { Controller, useForm } from "react-hook-form";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
type LeadValues = { plan: string };
export function PlanPicker() {
const { control, reset } = useForm<LeadValues>({
defaultValues: { plan: "" }, // "" not undefined: the Select stays controlled
});
return (
<form>
<Controller
control={control}
name="plan"
render={({ field }) => (
// value, not defaultValue: defaultValue is read once on mount,
// so reset() changes RHF state but the Select keeps showing the old pick.
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder="Choose a plan" />
</SelectTrigger>
<SelectContent>
<SelectItem value="free">Free</SelectItem>
<SelectItem value="pro">Pro</SelectItem>
</SelectContent>
</Select>
)}
/>
<button type="button" onClick={() => reset()}>Reset</button>
</form>
);
}The same applies to MUI's Select, Autocomplete and date pickers, React Select, and anything else rendered through Controller: render from field.value and give the field a non-undefined default.
4. File inputs and your own state
A file input registered with register() is cleared by reset(): the library sets the input's value to an empty string, and a no-argument reset() also calls the native form.reset(). What it can't reach is state you keep yourself: an image preview URL in useState, a drop-zone's file list, a character counter. Clear those in the same effect that calls reset(). Don't put a FileList in defaultValues.
5. You reset inside onSubmit
Covered above: handleSubmit writes isSubmitted and submitCount after your callback returns. Move the reset into the isSubmitSuccessful effect.
6. Field arrays come back with the wrong rows
reset({ items: [...] }) replaces useFieldArray rows too, but only if the array is part of defaultValues in the first place. Patterns for adding, removing and resetting rows are in the useFieldArray guide.
FAQ
How do I reset a React Hook Form after submit?
Give useForm complete defaultValues, then call reset() from a useEffect that watches formState.isSubmitSuccessful: useEffect(() => { if (isSubmitSuccessful) reset(); }, [isSubmitSuccessful, reset]). The React Hook Form docs recommend the effect over calling reset inside onSubmit because handleSubmit updates form state after your onSubmit returns. If the request fails, call setError('root.server', ...) instead of throwing: a root error makes isSubmitSuccessful false, so the user's input is not wiped.
Why is reset() not clearing my form?
The usual causes: you passed {} or a partial object (fields missing from the object are not cleared, and reset({}) also replaces your stored defaults with an empty object); a field has undefined as its default; a UI-library component is wired with defaultValue={field.value} instead of value={field.value}, so it ignores later updates; or you called reset inside onSubmit, where handleSubmit sets isSubmitted and submitCount after you. Pass a complete object of empty strings, or call reset() with no arguments, and render controlled components from field.value.
What is the difference between reset and resetField?
reset(values?, options?) resets the whole form: values, defaultValues (unless keepDefaultValues), errors, dirty and touched state, and the submit flags. resetField(name, options?) resets one registered field to its default and clears only that field's error, dirty and touched state. resetField's options are keepError (singular), keepDirty, keepTouched and defaultValue; reset's options are keepErrors (plural), keepDirty, keepDirtyValues, keepValues, keepDefaultValues and the other keep* flags.
How do I set default values from an API in React Hook Form?
Three options. Pass the fetched object to the values prop (since v7.41): the form resets itself whenever that object changes, and resetOptions: { keepDirtyValues: true } stops a refetch from overwriting fields the user is editing. Pass an async function as defaultValues to load once on mount, with formState.isLoading true until it resolves. Or call reset(data) in a useEffect after your own fetch. Never call reset during render.
Does reset() clear validation errors?
Yes. By default reset() clears errors, dirtyFields, touchedFields, isSubmitted, isSubmitSuccessful and submitCount. Pass { keepErrors: true } to keep the messages on screen, or the matching keep* option for any other piece of state you want to survive the reset.
Should I use reset or setValue to fill a form?
Use setValue to change one field as if the user typed it (optionally with shouldValidate, shouldDirty, shouldTouch). Use reset to replace the whole form and its baseline: after reset(data), data is the new defaultValues, so isDirty is false until the user edits something. For loading a record into an edit form, reset or the values prop is the right tool; looping over setValue leaves isDirty comparing against the old defaults.
Need somewhere to send the form? splitforms takes the fetch from the example above, emails you each submission, stores it in a dashboard and can forward it by webhook. No backend code, and the free plan includes 500 submissions total. Get a free access key, or copy a ready-made React form snippet.
