Concurrent submissions handling helps your app manage multiple form submissions at the same time without errors or data loss.
Concurrent submissions handling in Remix
export async function action({ request }) { const formData = await request.formData(); // process form data here return redirect('/success'); } // In the component const transition = useTransition(); const isSubmitting = transition.state === 'submitting';
The action function handles form submissions on the server side.
The useTransition hook tracks the submission state in the UI.
export async function action({ request }) { const formData = await request.formData(); const name = formData.get('name'); // Save name to database return redirect('/thank-you'); }
import { useTransition } from '@remix-run/react'; export default function ContactForm() { const transition = useTransition(); const isSubmitting = transition.state === 'submitting'; return ( <form method="post"> <input name="email" type="email" required /> <button type="submit" disabled={isSubmitting}> {isSubmitting ? 'Sending...' : 'Send'} </button> </form> ); }
export async function action({ request }) { const formData = await request.formData(); const email = formData.get('email'); // Check if email already exists to avoid duplicates const exists = await checkEmailExists(email); if (exists) { return json({ error: 'Email already registered' }, { status: 400 }); } await saveEmail(email); return redirect('/welcome'); }
This example prevents duplicate email submissions by storing emails in a Set. The submit button disables during submission to avoid multiple clicks. If the email is already registered, it shows an error message.
import { json, redirect } from '@remix-run/node'; import { useTransition, Form } from '@remix-run/react'; let emails = new Set(); export async function action({ request }) { const formData = await request.formData(); const email = formData.get('email'); if (emails.has(email)) { return json({ error: 'Email already registered' }, { status: 400 }); } emails.add(email); return redirect('/thank-you'); } export default function NewsletterForm() { const transition = useTransition(); const isSubmitting = transition.state === 'submitting'; const error = transition.type === 'actionReload' && transition.submission ? transition.submission.formData.get('error') : null; return ( <Form method="post" aria-label="Newsletter signup form"> <label htmlFor="email">Email:</label> <input id="email" name="email" type="email" required aria-required="true" /> <button type="submit" disabled={isSubmitting} aria-busy={isSubmitting}> {isSubmitting ? 'Submitting...' : 'Subscribe'} </button> {error && <p role="alert" style={{ color: 'red' }}>{error}</p>} </Form> ); }
Use useTransition to track submission state and disable inputs to prevent double submissions.
Always validate and check for duplicates on the server side to handle concurrent users.
Show clear feedback to users during and after submission for better experience.
Concurrent submissions handling prevents duplicate or conflicting form data.
Use server-side checks and client-side UI states together.
Disabling buttons and showing loading states improves user experience.
Practice
Solution
Step 1: Understand concurrent submissions
Concurrent submissions happen when a user submits a form multiple times quickly, causing duplicate or conflicting data.Step 2: Identify the purpose of handling them
Handling concurrent submissions prevents these duplicates by disabling the submit button or checking server-side if a submission is already in progress.Final Answer:
To prevent users from submitting the same form multiple times accidentally -> Option AQuick Check:
Concurrent submissions handling = prevent duplicates [OK]
- Thinking it speeds up server response
- Assuming it allows all submissions without checks
- Believing it disables inputs forever
Solution
Step 1: Understand disabling with state
In Remix, you track submission state with a variable likeisSubmittingto disable the button only during submission.Step 2: Choose the correct syntax
Usingdisabled={isSubmitting}disables the button dynamically, unlike hardcodedtrueorfalse.Final Answer:
<button disabled={isSubmitting}>Submit</button> -> Option AQuick Check:
Dynamic disable uses state variable [OK]
- Hardcoding disabled to true disables button always
- Using wrong state variable name
- Not disabling button at all
export const action = async ({ request }) => {
const formData = await request.formData();
const email = formData.get('email');
if (await isEmailRegistered(email)) {
return json({ error: 'Email already registered' }, { status: 400 });
}
await registerEmail(email);
return json({ success: true });
};Solution
Step 1: Analyze server-side check
The function checks if the email is registered before registering it, preventing duplicates if the first finishes first.Step 2: Understand race condition handling
If two submissions come close, the second will fail if the first completes and registers the email before the second checks.Final Answer:
The second submission fails if the first completes before it checks -> Option CQuick Check:
Server check prevents duplicates by failing second submission [OK]
- Assuming both succeed creating duplicates
- Thinking server throws syntax error
- Believing client disables form automatically
function MyForm() {
const transition = useTransition();
return (
<form method="post">
<button disabled={transition.state === 'submitting'}>Send</button>
</form>
);
}Users report the button sometimes stays disabled forever after submission. What is the likely cause?Solution
Step 1: Understand useTransition state
Thetransition.stateis 'submitting' while waiting for server response, then changes to 'idle' when done.Step 2: Identify cause of stuck state
If the server action never returns or hangs,transition.statestays 'submitting', keeping the button disabled forever.Final Answer:
The server action never returns a response, so transition.state stays 'submitting' -> Option BQuick Check:
Stuck 'submitting' means server response missing [OK]
- Disabling button on 'idle' disables it wrongly
- Changing form method unrelated to this issue
- Adding onClick handler won't fix stuck state
Solution
Step 1: Combine client and server handling
Disabling the submit button on the client prevents quick repeated clicks, improving user experience.Step 2: Add server-side validation
Server-side checks ensure data integrity by rejecting duplicates even if client-side fails or is bypassed.Final Answer:
Disable submit button on client during submission and check server if data already exists before saving -> Option DQuick Check:
Client disable + server check = robust duplicate prevention [OK]
- Relying only on client disables risks server duplicates
- Relying only on server allows bad UX with multiple clicks
- Using setTimeout delays but does not prevent duplicates
Ready to prove it?
Pass the test and this pattern moves unknown
