My users were complaining that clicking the 'Save Profile' button sometimes reverted their changes back to old data. It turned out to be a classic race condition where an older network request finished after a newer one.
- How fast double-clicks break single-page applications.
- Why Inertia.js form submissions can overlap.
- How to use abort controllers to cancel stale requests.
- A simple way to disable buttons during form submission.
What the user experienced
To the person using the app, the bug looked random. They would update their username, quickly click save twice, and see their old username reappear on the screen. The database actually had the correct new data, but the screen showed the old data.
This happens because network requests do not always finish in the order you send them. The first slow request finished after the second fast request, overwriting the page with stale data.
Diagnosing the race condition step by step
I opened the Network tab in Chrome Developer Tools and throttled my connection to 'Fast 3G'. Chrome Developer Tools is a built-in browser panel for debugging web applications. I clicked the save button twice in rapid succession.
I watched two PATCH requests fly off to the Laravel backend. The first request took 800 milliseconds, and the second took 300 milliseconds. The second request finished first, updating the database correctly. Then the first request finished and returned the old server state to React, breaking the UI.
The fix using Inertia.js and React
Inertia.js is a library that lets you build single-page apps using classic server-side routing. To fix this, I needed to ensure my React component handled loading states correctly and prevented duplicate submissions.
Here is the updated React component using the built-in processing flag from Inertia:
import { useForm } from '@inertiajs/react';
export default function ProfileEdit({ user }) {
const { data, setData, patch, processing } = useForm({
name: user.name,
});
function submit(e) {
e.preventDefault();
patch('/profile');
}
return (
<form onSubmit={submit}>
<input
type="text"
value={data.name}
onChange={e => setData('name', e.target.value)}
/>
<button type="submit" disabled={processing}>
{processing ? 'Saving...' : 'Save'}
<button>
</form>
);
}Disabling the submit button while processing prevents the user from firing multiple requests in the first place. This completely stops the race condition at the user interaction level.
Common mistakes
Developers often rely only on backend database transactions to solve UI bugs. Databases handle data integrity well, but they cannot control the order in which network packets arrive in the browser. Always lock your user interface controls during an active request to prevent accidental duplicate submissions.
Open your current project right now and check your form submit buttons. Add a disabled state to any button that submits data if you haven't already.