Async Username Validation in Ant Design Form: A Practical Guide
Add a server-side username availability check to Ant Design Form: a rules validator that returns a Promise, shows a loading state, blocks submit on duplicates, and cancels stale requests.
21 Dec 2025, 19:16 UTC

Checking whether a username is already taken looks trivial until the check has to reach your backend. A synchronous Ant Design validator cannot wait for a network round trip, and a hastily written asynchronous one tends to fire a request on every keystroke, leave the field stuck in a loading state, or try to update state after the component is gone. The reliable pattern is a Form.Item rules validator that returns a Promise, combined with a deliberate trigger and request cleanup.
What the finished field should do
Before writing code, fix the acceptance criteria. The username input should query your availability endpoint, show a validating spinner while the request is in flight, block form submission when the name is taken, and render the error message under the field. When the user leaves the field with a unique name, validation resolves and the submit button works. None of this replaces a server-side uniqueness constraint; it is a user-experience guard only.
Before you start
You need a React function component that already uses Ant Design Form, and a backend route that reports availability for a name, for example GET /api/usernames/available?name=... returning { "available": true }. If Ant Design is not yet a dependency, install it from your project root with npm install antd (or the equivalent for pnpm or yarn); no special permissions are required beyond normal package installation. These examples target Ant Design v4 or v5. The rules.validator Promise API is the same in both, but v5 restyles defaults through design tokens, so confirm the spinner and error colors against the version you have installed.
Two terms matter here. validateTrigger is the DOM event that starts validation for a field; it defaults to onChange. The validator is a function inside a rule that receives (rule, value) and may return a Promise; rejecting that Promise with an Error turns the error message into the field's validation text.
Wire the async validator into Form.Item
Give the Form.Item a name, a rules array, and hasFeedback so the validating spinner is visible. Inside the validator, skip empty values so the required rule owns that case, then call the endpoint. Resolve when the name is free and reject with a clear message when it is taken.
import { Form, Input, Button } from 'antd';
import { useEffect, useRef } from 'react';
async function isUsernameAvailable(name, signal) {
const res = await fetch(
`/api/usernames/available?name=${encodeURIComponent(name)}`,
{ signal }
);
if (!res.ok) {
throw new Error('Availability service unavailable');
}
const body = await res.json();
return body.available === true;
}
export default function SignupForm() {
const [form] = Form.useForm();
const abortRef = useRef(null);
useEffect(() => () => {
if (abortRef.current) abortRef.current.abort();
}, []);
const onFinish = (values) => {
console.log(values);
};
return (
<Form form={form} onFinish={onFinish} layout="vertical">
<Form.Item
name="username"
hasFeedback
validateTrigger="onBlur"
rules={[
{ required: true, message: 'Username is required' },
{
validator: async (_, value) => {
if (!value) return;
if (abortRef.current) abortRef.current.abort();
const controller = new AbortController();
abortRef.current = controller;
try {
const available = await isUsernameAvailable(value, controller.signal);
if (!available) {
throw new Error('This username is already taken');
}
} catch (err) {
if (err.name === 'AbortError') return;
throw new Error('Could not verify username. Please try again.');
}
},
},
]}
>
<Input placeholder="Choose a username" autoComplete="off" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Create account
</Button>
</Form.Item>
</Form>
);
}
The AbortController stored in a ref cancels the previous in-flight request before a new one starts, and a cleanup effect aborts it on unmount. That prevents stale responses from overwriting a newer result and avoids work after the component is gone. React 18 no longer logs the "setState on unmounted component" warning, but canceling is still the correct behavior; on React 17 or earlier, also guard with a mounted flag if you see the warning.
Choose the trigger on purpose
The default onChange trigger validates on every keystroke, which for a network check means a request per character. For username availability, onBlur is usually the better engineering decision: the check runs once when the user leaves the field, the loading state is meaningful, and you avoid racing responses for partially typed names. If product requirements demand live feedback while typing, keep onChange but debounce the network call inside the validator and abort superseded requests, as the example above already does.
Check the behavior
- Type a name known to be taken, then blur the field: a spinner appears, then the error "This username is already taken" renders under the input and the submit button stays disabled.
- Type a unique name and blur: the validator resolves, no error shows, and submitting the form calls
onFinish. - Leave the field and return with a new value: the previous request is aborted and only the latest result is applied.
- Unmount the form while a request is pending: no warning about updating an unmounted component appears in the console.
When it goes wrong: recovery options
Too many requests
If the endpoint logs a spike, switch validateTrigger to onBlur first. If you must validate on change, debounce the call (for example, 300-500 ms) and keep the abort logic so only the final keystroke's request survives.
Stuck loading state
A field that never leaves the validating state almost always means the validator's Promise never settled. Always resolve or reject explicitly, and wrap the network call in try/catch so transport failures reject with a friendly message instead of hanging. Never leave a branch that returns without resolving.
Error text never appears
Reject with an Error object, not a string or a plain object. The validation layer reads error.message to populate the field. If you reject with something else, the message may be dropped and the field can look permanently valid.
Limits of a client-side check
This validation is a convenience, not a guarantee. Users can bypass the form and call your API directly, and two people can pass the check simultaneously before either submits. The backend must enforce uniqueness independently, ideally with a unique index and a clear error on the create request. Treat the async validator as a way to fail fast in the UI, and let the server remain the source of truth.
A quick way to verify
Build a minimal form with one Form.Item and point the fetch at a mock that resolves after a short delay, returning available: false for one name and true for others. Walk the four checks above. Because the visual result depends on your Ant Design version and theme tokens, confirm the spinner and error styling in your own app rather than relying on defaults.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.