React Forms
Building forms in React usually requires handling submissions asynchronously (AJAX) to prevent a full page reload. This allows you to show custom loaders, validation errors, and success states directly on the page.
AI Prompt (Cursor, Claude, v0, ChatGPT)
Section titled “AI Prompt (Cursor, Claude, v0, ChatGPT)”If you are using an AI coding assistant (such as Cursor, Claude, v0, Windsurf, or ChatGPT) to generate your React components, use this ready-to-use prompt:
Create a responsive React contact form component.
Submission Endpoint:https://api.lucidforms.co/f/{your-form-id}
Requirements:- Fields: Name, Email, and Message with proper input validation- Include an invisible honeypot field (_honeypot) for spam bot detection- Submit form data asynchronously via fetch() to the submission endpoint using JSON- Include 'Content-Type: application/json' and 'Accept: application/json' headers- Manage reactive UI states with useState: idle, loading (disable submit button), success, and error- Reset form fields upon successful submission- Style with responsive Tailwind CSS classes and dark mode supportControlled Component Example
Section titled “Controlled Component Example”Here is a copy-pasteable example of a React contact form component using standard hooks (useState) and the browser fetch API:
import React, { useState } from 'react';
export default function ContactForm() { const [formState, setFormState] = useState({ name: '', email: '', message: '', _honeypot: '', // Honeypot spam trap });
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); const [responseMessage, setResponseMessage] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { setFormState({ ...formState, [e.target.name]: e.target.value, }); };
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setStatus('loading');
// Bot trap check: if filled, abort or handle as spam if (formState._honeypot) { setStatus('success'); return; }
try { const response = await fetch('https://api.lucidforms.co/f/{your-form-id}', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify({ name: formState.name, email: formState.email, message: formState.message, }), });
const data = await response.json();
if (response.ok) { setStatus('success'); setResponseMessage('Thank you! Your message has been sent.'); setFormState({ name: '', email: '', message: '', _honeypot: '' }); } else { setStatus('error'); setResponseMessage(data.error || 'Something went wrong. Please try again.'); } } catch (error) { setStatus('error'); setResponseMessage('Network error. Please check your connection and try again.'); } };
return ( <div style={{ maxWidth: '400px', margin: '0 auto', fontFamily: 'sans-serif' }}> <h2>Contact Us</h2>
{status === 'success' && ( <div style={{ color: 'green', marginBottom: '15px' }}>{responseMessage}</div> )} {status === 'error' && ( <div style={{ color: 'red', marginBottom: '15px' }}>{responseMessage}</div> )}
<form onSubmit={handleSubmit}> {/* Honeypot field (hidden from view) */} <input type="text" name="_honeypot" value={formState._honeypot} onChange={handleChange} style={{ display: 'none' }} tabIndex={-1} autoComplete="off" />
<div style={{ marginBottom: '12px' }}> <label htmlFor="name" style={{ display: 'block', marginBottom: '4px' }}>Name</label> <input type="text" id="name" name="name" value={formState.name} onChange={handleChange} required style={{ width: '100%', padding: '8px', boxSizing: 'border-box' }} /> </div>
<div style={{ marginBottom: '12px' }}> <label htmlFor="email" style={{ display: 'block', marginBottom: '4px' }}>Email</label> <input type="email" id="email" name="email" value={formState.email} onChange={handleChange} required style={{ width: '100%', padding: '8px', boxSizing: 'border-box' }} /> </div>
<div style={{ marginBottom: '16px' }}> <label htmlFor="message" style={{ display: 'block', marginBottom: '4px' }}>Message</label> <textarea id="message" name="message" value={formState.message} onChange={handleChange} required rows={4} style={{ width: '100%', padding: '8px', boxSizing: 'border-box' }} /> </div>
<button type="submit" disabled={status === 'loading'} style={{ padding: '10px 16px', backgroundColor: '#0070f3', color: 'white', border: 'none', cursor: 'pointer', opacity: status === 'loading' ? 0.6 : 1 }} > {status === 'loading' ? 'Sending...' : 'Send Message'} </button> </form> </div> );}Best Practices
Section titled “Best Practices”AJAX Request Headers
Section titled “AJAX Request Headers”When submitting data programmatically via AJAX, make sure to add the correct headers:
'Content-Type': 'application/json'to indicate you are sending JSON data.'Accept': 'application/json'to instruct Lucid Forms to reply with a JSON response instead of a redirect.
Handling File Uploads
Section titled “Handling File Uploads”If your React form includes files:
- Do not send JSON.
- Use the standard
FormDataobject instead. - Remove the
'Content-Type'header so the browser sets it automatically with the multipart boundary. - Ensure files are under the size limits of your plan (up to 5MB per file, 1GB total account storage on Pro).

