Guide
React Integration
Build beautiful, type-safe forms with React hooks.
This guide covers React 18+ with hooks. We'll build a reusable form component with loading states, validation, and proper TypeScript types.
Installation
No additional packages required - just your React app and fetch.
bash
# Create a new React app (if starting fresh)
npm create vite@latest my-app -- --template react-ts
cd my-app
# Or use your existing project
npm installCreate the Submit Hook
First, let's create a reusable hook for form submissions:
hooks/use-form-submit.ts
import { useState, useCallback } from "react";
interface SubmitOptions {
formSlug: string;
}
interface SubmitState {
status: "idle" | "loading" | "success" | "error";
error: string | null;
leadId: string | null;
}
export function useFormSubmit({ formSlug }: SubmitOptions) {
const [state, setState] = useState<SubmitState>({
status: "idle",
error: null,
leadId: null,
});
const submit = useCallback(
async (data: Record<string, unknown>) => {
setState({ status: "loading", error: null, leadId: null });
try {
const response = await fetch("/api/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ formSlug, ...data }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "Submission failed");
}
const result = await response.json();
setState({ status: "success", error: null, leadId: result.id });
return result;
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
setState({ status: "error", error: message, leadId: null });
throw err;
}
},
[formSlug]
);
const reset = useCallback(() => {
setState({ status: "idle", error: null, leadId: null });
}, []);
return {
...state,
isLoading: state.status === "loading",
isSuccess: state.status === "success",
isError: state.status === "error",
submit,
reset,
};
}Server API Route
Create a server-side API route to securely add your API key:
app/api/submit/route.ts
// For Next.js App Router: app/api/submit/route.ts
export async function POST(request: Request) {
const body = await request.json();
const { formSlug, ...data } = body;
const response = await fetch(
process.env.ONEDB_API_URL + "/v1/leads",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.ONEDB_API_KEY,
},
body: JSON.stringify({
formSlug,
...data,
}),
}
);
const result = await response.json();
return Response.json(result, { status: response.status });
}
// For Vite/CRA: Use a serverless function or backend APIContact Form Component
Now build the form using our hook:
components/ContactForm.tsx
import { useState } from "react";
import { useFormSubmit } from "@/hooks/use-form-submit";
interface FormData {
name: string;
email: string;
company: string;
message: string;
}
const initialData: FormData = {
name: "",
email: "",
company: "",
message: "",
};
export function ContactForm() {
const [formData, setFormData] = useState<FormData>(initialData);
const { submit, isLoading, isSuccess, isError, error, reset } = useFormSubmit({
formSlug: "contact-form",
});
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
await submit(formData);
setFormData(initialData);
} catch {
// Error handled by hook
}
};
if (isSuccess) {
return (
<div className="success-message">
<h3>Thanks for reaching out!</h3>
<p>We'll get back to you soon.</p>
<button onClick={reset}>Send another message</button>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="contact-form">
<div className="form-field">
<label htmlFor="name">Name *</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
disabled={isLoading}
/>
</div>
<div className="form-field">
<label htmlFor="email">Email *</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
disabled={isLoading}
/>
</div>
<div className="form-field">
<label htmlFor="company">Company</label>
<input
type="text"
id="company"
name="company"
value={formData.company}
onChange={handleChange}
disabled={isLoading}
/>
</div>
<div className="form-field">
<label htmlFor="message">Message</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
rows={4}
disabled={isLoading}
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? "Sending..." : "Send Message"}
</button>
{isError && (
<p className="error-message">{error}</p>
)}
</form>
);
}With React Hook Form
For more complex forms, use React Hook Form with Zod validation:
bash
npm install react-hook-form zod @hookform/resolverscomponents/ContactFormAdvanced.tsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useFormSubmit } from "@/hooks/use-form-submit";
const schema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Invalid email address"),
company: z.string().optional(),
message: z.string().min(10, "Message must be at least 10 characters"),
});
type FormData = z.infer<typeof schema>;
export function ContactFormAdvanced() {
const { submit, isLoading, isSuccess, reset: resetSubmit } = useFormSubmit({
formSlug: "contact-form",
});
const {
register,
handleSubmit,
reset: resetForm,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(schema),
});
const onSubmit = async (data: FormData) => {
try {
await submit(data);
resetForm();
} catch {
// Error handled by hook
}
};
if (isSuccess) {
return (
<div className="success">
<p>Thanks! We'll be in touch.</p>
<button onClick={() => { resetSubmit(); resetForm(); }}>
Send another
</button>
</div>
);
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Name *</label>
<input {...register("name")} disabled={isLoading} />
{errors.name && <span className="error">{errors.name.message}</span>}
</div>
<div>
<label>Email *</label>
<input type="email" {...register("email")} disabled={isLoading} />
{errors.email && <span className="error">{errors.email.message}</span>}
</div>
<div>
<label>Company</label>
<input {...register("company")} disabled={isLoading} />
</div>
<div>
<label>Message *</label>
<textarea {...register("message")} rows={4} disabled={isLoading} />
{errors.message && <span className="error">{errors.message.message}</span>}
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? "Sending..." : "Send Message"}
</button>
</form>
);
}Pro Tips
- Use optimistic UI updates for instant feedback
- Add analytics tracking on successful submissions
- Consider using React Query or SWR for advanced state management
- Test forms with Playwright or Cypress for E2E coverage