React Hook Form: Fix Confirm Password Validation
Block mismatched submissions, display the field error, and catch a confirmation that becomes stale after the user edits the original password.
TL;DR#
A required confirmation field can contain a different password and still pass validation. Add a validate callback to the confirmation registration, compare its value with getValues('password'), and return an error message when they differ. For immediate feedback when the original password changes, register that original field with deps: ['confirmPassword'] and use an appropriate validation mode. The working example below uses onChange.
The message is application-defined, not an exception from React Hook Form:
Passwords do not matchThe original Stack Overflow question asks how to compare these fields and show the error. Its answers offer both a schema resolver and a registered validation callback. This article follows the latter for native inputs, then tests the easily missed case where a previously matching confirmation becomes wrong.
Why it happens#
Required checks answer whether a field has a value. They do not express a relationship between two fields. Naming an input confirmPassword, putting it beside password, or giving both the password input type does not create that relationship either. The callback must read the other value and return the comparison result.
The register reference defines the custom validator contract: a successful result can be true, whereas a returned string becomes an error message. That makes the comparison concise: equality produces true; inequality produces the message you want to display. Returning a message on success would mark the field invalid instead.
Reading the other field and scheduling another validation are separate operations. getValues reads the form's current values without subscribing to changes. Calling it inside the confirmation validator gives that run the current password. It does not, by itself, arrange another run when the password input changes later. The difference matters when the user fills both fields correctly, goes back to edit the first, and sees no new confirmation error.
Fix#
1. Keep a single registration name for each field#
Use password and confirmPassword consistently in the registrations, the value lookup, dependencies, and error display. If your existing form uses cpassword, either keep that name throughout or rename every reference. An error stored under one name will not appear when JSX reads another.
For an existing React application, install the form package if it is missing:
npm install react-hook-formThis example uses the built-in registered-input rules. If your form already has a resolver, keep the comparison in that schema instead of stacking two competing validation approaches. The useForm resolver documentation describes schema integration and its limitation on mixing a resolver with built-in validators. You do not need to introduce a resolver solely for this comparison.
2. Register the comparison and its dependency#
Save this as PasswordForm.jsx and render it in your React application. The client directive also makes the component an explicit client entry point when used in a Next.js App Router project. This is a local validation demonstration: its submission callback reports that validation passed and deliberately performs no account creation or password update.
'use client';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
export default function PasswordForm() {
const [submitted, setSubmitted] = useState(false);
const { register, getValues, handleSubmit, formState: { errors } } = useForm({
mode: 'onChange',
defaultValues: { password: '', confirmPassword: '' },
});
return (
<form noValidate onSubmit={handleSubmit(() => setSubmitted(true))}>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
autoComplete="new-password"
{...register('password', {
required: 'Enter a password',
deps: ['confirmPassword'],
})}
/>
{errors.password && <p role="alert">{errors.password.message}</p>}
<label htmlFor="confirmPassword">Confirm password</label>
<input
id="confirmPassword"
type="password"
autoComplete="new-password"
{...register('confirmPassword', {
required: 'Confirm your password',
validate: (value) => value === getValues('password') || 'Passwords do not match',
})}
/>
{errors.confirmPassword && <p role="alert">{errors.confirmPassword.message}</p>}
<button type="submit">Check passwords</button>
{submitted && <p role="status">Submission passed validation. No password was saved.</p>}
</form>
);
}The dependency is intentionally attached to the original password registration. It asks for the confirmation to be validated when that original field's registered validation runs. Putting deps: ['password'] on the confirmation field describes the reverse direction, which is not the stale-confirmation problem being fixed.
Leave the props returned by register connected to the actual inputs. They include the change and blur handlers and the input ref. A custom input wrapper must forward them correctly; overwriting a handler after spreading the registration can disconnect the event the form needs. If a wrapper instead crashes with a missing callback, the prop-name mismatch fix explains how to check the name passed by the parent against the name read by the child.
3. Render the message rather than the error object#
The example reads errors.confirmPassword.message. Keep the conditional wrapper: there is no error object to read before a failure. Rendering errors.confirmPassword itself tries to display an object, which can lead to the separate Objects Are Not Valid as a React Child error.
The status message records a successful submission event. It is not a live assertion that the current fields still match after further edits. Reload the demonstration between the submission scenarios below so that a historical status does not obscure what is being tested. In a real form, use the submit callback for the intended operation and present its result separately from field-validation state.
Verify the fix#
The component above was reproduced on Windows with Node v26.7.0, React and React DOM 19.2.4, React Hook Form 7.91.0, and jsdom 26.1.0 on 28 October 2026. This was a native Node and DOM-test run, not Docker or a real browser. The harness compiled the same JSX, dispatched input and submit events, and asserted the rendered messages. It did not send a password to any service or log input values.
The first comparison removed only the confirmation validator from the component. Different non-empty fields then reached the valid-submit callback. Restoring that one validator blocked the same input. A separate comparison removed only the dependency option to isolate the stale-confirmation behaviour. These were the observed result lines:
BEFORE: mismatched fields reached the valid-submit callback
AFTER: mismatched fields blocked; message = Passwords do not match
MATCH: valid-submit callback ran
WITHOUT DEPS: editing the original field left confirmation error absent until submission
WITHOUT DEPS: submitting still blocked the mismatch
WITH DEPS: editing the original field immediately showed the mismatch
EMPTY: both required messages shown; submission blockedCheck the same transitions in your application. Start with empty fields and submit: each required message should appear. Next enter different non-empty values: the comparison should fail and the success callback should not run. Make them equal and submit again: validation should pass. Finally, reload, enter matching values, and change only the original password. The confirmation error should appear without editing the confirmation field again.
That last check distinguishes the dependency fix from a validator that works only at submission time. Without the dependency, submission still reruns the confirmation validator and blocks the mismatch; the defect is stale feedback between edits, not an unconditional bypass of submit validation.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Why onTouched behaves differently after a password edit#
The source question uses onTouched, whereas the example deliberately uses onChange. The validation-mode reference distinguishes first-blur validation from validation on each change. Dependencies do not make an untouched original field ignore its own mode.
Changing only the mode in the reproduced component showed this distinction. Both fields were filled without a blur event, then the original was changed. No confirmation error appeared until the original input was blurred:
ON TOUCHED: original field edits before its first blur did not run dependency validation
ON TOUCHED: blurring the original field ran dependency validationChoose the feedback timing deliberately. Immediate comparison can also display the confirmation-required message as soon as the user starts filling the first field. That is a consequence of validating its dependent field, not evidence that the comparison is broken. If your product should wait for a particular interaction, specify that interaction and test it rather than copying a mode name and expecting identical timing.
Programmatic updates need their own check too. The trigger reference provides explicit field validation; do not assume that an arbitrary state update behaves like the registered input event tested here. For a programmatic password change, explicitly validate the confirmation field when that is the feedback your flow requires.
Matching fields does not create an account#
This example proves equality and presence, not password strength or a completed password reset. It intentionally avoids copying the short length limits from the original question as an account-security policy. Apply the requirements of the authentication service that actually accepts the password, and keep those requirements consistent between the form and the operation that saves it.
Client-side messages are also separate from server-side input checks. If the form submits to a Next.js Server Action backed by Supabase, the Server Actions validation guide covers returning structured server errors to the form. Keep the confirmation comparison, the server's acceptance criteria, and the final success response as distinct checks, so a green local form never gets mistaken for a password that has already been stored.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix "changing an uncontrolled input to be controlled"
Why an empty state object makes React treat a text field as uncontrolled, what the React 19 wording looks like, and the react-hook-form variant of the same warning.
React Hook useEffect has a missing dependency: the fix
The three warnings this rule prints for one fetch function, which remedy each one wants, and a measured run showing what silencing it costs when a prop changes.
Fix: Objects Are Not Valid as a React Child
The 2015 bug reproduced today, the exact modern error text, what the production build shows instead, and the one-line fix for every cause.


