Reset After Success
Three canonical patterns for post-submission UX: redirect, in-place reset with a toast, and showing a success state. Choosing the wrong one leads to subtle bugs.
Why it matters
"The form doesn't clear after I submit" is one of the most common issues raised by developers new to the library. The reason is that isSubmitSuccessful stays true indefinitely, and reset() needs to be called explicitly. These are intentional RHF behaviours — but they need to be wired correctly.
There are three distinct patterns depending on your UX goal. Pick the right one and you avoid ghost states, stale data, and infinite success screens.
Pattern 1 — Redirect after submit
Best for create-and-navigate flows: create a post, place an order, complete onboarding. The server calls redirect() and the page navigates away — no client-side reset needed.
'use server'
import { redirect } from 'next/navigation'
import { withZod } from 'hookform-action-core/with-zod'
import { postSchema } from './schema'
export const createPostAction = withZod(postSchema, async (data) => {
const post = await db.posts.create({ data })
// redirect() throws internally — the return below is unreachable
redirect(`/posts/${post.id}`)
})redirect() the hook sets isSubmitSuccessful = true, but the page navigates away immediately so this state is never rendered. You do not need to handle it on the client.Pattern 2 — In-place reset with onSuccess
Best for forms that stay on the same page after submission: comment boxes, subscription forms, quick-add widgets. Use onSuccess to call reset() and optionally show a toast.
'use client'
import { useActionForm } from 'hookform-action'
import { addCommentAction } from './actions'
export function CommentForm() {
const {
register,
handleSubmit,
reset,
formState: { errors, isPending },
} = useActionForm(addCommentAction, {
defaultValues: { text: '' },
onSuccess: () => {
reset() // Clears all fields back to defaultValues
// toast.success('Comment posted!') // optional side effect
},
})
return (
<form onSubmit={handleSubmit()}>
<textarea {...register('text')} placeholder="Write a comment…" />
{errors.text && <p className="text-red-400">{errors.text.message}</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Posting…' : 'Post Comment'}
</button>
</form>
)
}onSuccess: (result) => {
// Pass new values to reset() — useful after an edit form
// where the server returns the updated record
reset({
title: result.data.title,
body: result.data.body,
})
}Pattern 3 — isSubmitSuccessful guard
Best for single-use forms where you want to replace the form with a success message: contact forms, RSVP forms, one-time redemptions.
'use client'
import { useActionForm } from 'hookform-action'
import { sendMessageAction } from './actions'
export function ContactForm() {
const {
register,
handleSubmit,
formState: { errors, isPending, isSubmitSuccessful },
} = useActionForm(sendMessageAction, {
defaultValues: { name: '', email: '', message: '' },
})
// Replace the form entirely once submitted
if (isSubmitSuccessful) {
return (
<div className="text-center py-12">
<p className="text-2xl font-bold text-green-400">Message sent!</p>
<p className="text-gray-400 mt-2">We'll get back to you within 24 hours.</p>
</div>
)
}
return (
<form onSubmit={handleSubmit()}>
<input {...register('name')} placeholder="Your name" />
{errors.name && <p className="text-red-400">{errors.name.message}</p>}
<input type="email" {...register('email')} placeholder="Email" />
{errors.email && <p className="text-red-400">{errors.email.message}</p>}
<textarea {...register('message')} placeholder="Your message" />
{errors.message && <p className="text-red-400">{errors.message.message}</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Sending…' : 'Send Message'}
</button>
</form>
)
}Bonus — Clearing persistence after wizard submit
When using persistKey, always call clearPersistedData() in onSuccess to remove the saved draft from sessionStorage. Otherwise the user will see stale data if they open the form again.
const { handleSubmit, clearPersistedData } = useActionForm(wizardAction, {
defaultValues: { ... },
persistKey: 'onboarding-wizard',
onSuccess: () => {
clearPersistedData() // ← remove draft from sessionStorage
router.push('/dashboard')
},
})Key Concepts
reset()Resets all fields to defaultValues, clears all errors, and resets RHF state (isDirty, isSubmitSuccessful, etc.). reset(newValues) additionally updates the stored default values — useful after editing a record.
isSubmitSuccessfulBecomes true after a successful action call (no field errors returned) and stays true until reset() is called or the component unmounts. This is intentional — use it as the guard for your success UI.
onSuccess callbackReceives the full action result and fires only when the action succeeds (no field errors). The best place to call reset(), show a toast, or trigger a router navigation.
⚠️ Pitfalls
- ⚠
Calling
reset()in auseEffectwatchingisSubmitSuccessfulThis creates a double-render cycle. Prefer
onSuccess— it fires synchronously after the action resolves, before any re-render. - ⚠
Resetting inside a modal — flash of empty form
If you
reset()and then close the modal inonSuccess, the form briefly shows empty before the modal animation completes. Prefer resetting in theonClosehandler instead, or use akeyprop to remount. - ⚠
reset()without arguments on an edit formIf
defaultValuescame from the server and the user just saved new data, callingreset()reverts to the old server data. Usereset(result.data)to update the baseline after a successful edit.