Skip to content

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.


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>
);
}

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.

If your React form includes files:

  1. Do not send JSON.
  2. Use the standard FormData object instead.
  3. Remove the 'Content-Type' header so the browser sets it automatically with the multipart boundary.
  4. Ensure files are under the size limits of your plan (up to 1GB backed by Google Drive on Pro).