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.
Controlled 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 1GB backed by Google Drive on Pro).