Build a contact form in React
React manages the interaction while a published endpoint handles intake. Track pending, validation, network, and success states explicitly, and do not clear the form until the accepted response is confirmed.
Create a free formA copyable example
// Browser-side React component
import { useState, useRef } from 'react';
export default function Contact() {
const [etat, setEtat] = useState('');
const [busy, setBusy] = useState(false);
const tentative = useRef({ contenu: '', id: '' });
async function envoyer(event) {
event.preventDefault();
const form = event.currentTarget;
if (!form.reportValidity()) return;
const data = new FormData(form);
const fields = { name: data.get('name'), email: data.get('email'), message: data.get('message') };
const contenu = JSON.stringify(fields);
if (contenu !== tentative.current.contenu) tentative.current = { contenu, id: crypto.randomUUID() };
setBusy(true); setEtat("Sending…");
try {
const response = await fetch("https://littlereply.com/api/submit", {
method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': tentative.current.id },
body: JSON.stringify({ form_key: "YOUR_ACCESS_KEY", fields })
});
const result = await response.json();
if (!response.ok) throw new Error(response.status === 429 ? "Limit reached. Try again later." : "Could not submit. Check the fields and try again.");
setEtat("Your message was saved. Email delivery is tracked separately.");
} catch (error) { setEtat(error instanceof Error ? error.message : "Could not submit."); }
finally { setBusy(false); }
}
return <form onSubmit={envoyer}>
<label>Your name<input name='name' autoComplete='name' required /></label>
<label>Your email<input name='email' type='email' autoComplete='email' required /></label>
<label>Your message<textarea name='message' required maxLength={5000} /></label>
<button disabled={busy}>Send message</button>
<p role='status' aria-live='polite'>{etat}</p>
</form>;
}The key YOUR_ACCESS_KEY is a placeholder to replace with your form’s public key. The code contains no server key or destination address.
Keep state recoverable
Use controlled or form-managed fields, disable the submit button only while the request is active, and keep values when the server rejects a field or the network fails.
Generate one attempt identifier for a submission and reuse it only while retrying the same values. Generate a new identifier after the visitor edits the request.
Read the response contract
Handle 422 as a field or schema problem, 429 as a rate or quota limit, and 5xx or network failures as retryable only when the outcome is known. Do not convert an uncertain result into an automatic duplicate send.
After 202, render a truthful stored confirmation. Email and integration results remain available in the delivery history.
Keep private settings out of the bundle
The browser may contain the public form key. Recipients, provider keys, management tokens, and connection secrets remain in the private workspace or server environment.
Try the template before creating an account
Local preview: changes stay in this browser. No message or email is sent.
HTML contact
Name, email, and message: the essentials for starting a conversation.
LittleReply<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Contact form</title>
<style>form[data-littlereply]{max-width:640px;margin:2rem auto;padding:1.5rem;font:16px/1.6 system-ui,sans-serif;color:#202033;background:white;border:1px solid #e4e4ee;border-radius:16px;box-sizing:border-box}form[data-littlereply] label{display:block;margin-top:1rem;font-weight:600}form[data-littlereply] input,form[data-littlereply] textarea,form[data-littlereply] select{box-sizing:border-box;width:100%;padding:.75rem;font:inherit;border:1px solid #727285;border-radius:8px;background:white;color:#202033}form[data-littlereply] input[type=checkbox],form[data-littlereply] input[type=radio]{width:auto}form[data-littlereply] button{margin-top:1rem;padding:.8rem 1.1rem;font:inherit;font-weight:600;border:0;border-radius:8px;background:var(--form-accent,#4f46e5);color:white;cursor:pointer}form[data-littlereply] :focus-visible{outline:3px solid #f59e0b;outline-offset:3px}form[data-littlereply] button:disabled{opacity:.6}form[data-littlereply] [role=status]{min-height:1.6em}form[data-littlereply] fieldset{border:0;padding:0}form[data-littlereply] .privacy-notice{font-size:.875rem;color:#4b4b60}
form[data-littlereply]{--form-accent:#4f46e5;--form-ink:#ffffff}
form[data-littlereply] button{color:var(--form-ink)}form[data-littlereply] button[data-fq-back]{background:#eee;color:#202033}form[data-littlereply] [hidden]{display:none}</style></head><body>
<form action="https://littlereply.com/api/submit" method="POST" data-littlereply="YOUR_ACCESS_KEY" data-locale="en" data-success="Your message has been stored. We will reply soon.">
<input type="hidden" name="_fq_key" value="YOUR_ACCESS_KEY" />
<input type="text" name="botcheck" tabindex="-1" autocomplete="off" style="display:none" aria-hidden="true" />
<label for="fq_name">Your name *</label>
<input type="text" name="name" id="fq_name" required autocomplete="name" />
<label for="fq_email">Your email address *</label>
<input type="email" name="email" id="fq_email" required autocomplete="email" />
<label for="fq_message">Your message *</label>
<textarea name="message" id="fq_message" required maxlength="5000" rows="5"></textarea>
<button type="submit">Send my message</button>
<p role="status" aria-live="polite"></p>
</form>
<!-- Accessible AJAX errors and duplicate-safe retries -->
<script src="https://littlereply.com/client/v1.js" data-api="https://littlereply.com" defer></script>
</body></html>Replace YOUR_ACCESS_KEY with your form's public key. The code includes its styles and an AJAX enhancement; the native HTML POST also works without JavaScript.
Sources
Frequently asked questions
Should I use a React form library?
It is optional. The transport and response contract work with native FormData or a form library.
When should I clear the form?
After a confirmed accepted response, never before the request outcome is known.