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 install

Create 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 API

Contact 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/resolvers
components/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

Next Steps