Skip to content
Working With React

A React form component

Build a React contact form with Qubeforms using controlled inputs and FormData. Configure your endpoint, submit fields, handle errors, and display success feedback.

1. Add the external form stylesheet

Save the CSS below as a separate file in your website. HTML examples load it from /static/assets/css/form-helper.css; React and Vue import ./form-helper.css beside the component. Adjust the path to match your site and confirm the file loads before publishing. The generic .form-helper class moves the text input off-screen without the hidden attribute or display: none. Keep tabindex="-1", autocomplete="off", and aria-hidden="true". New forms use fax_number; existing forms keep their saved name. Match Settings → Delivery & spam → Honeypot field in HTML, JSON, and drafts. To rename an existing trap, update both the setting and the website together. Never use a name that also collects a real visitor answer.

.form-helper {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
    pointer-events: none;
}

2. Connect your React site

Use this JavaScript/JSX component in a React application. Create an active Qubeforms form and copy its full public submission endpoint. Under Settings → General, allow the origin hosting your React site, including its development port. Ordinary submissions work on every plan; attachments require Starter or higher. The form uses browser validation and a live status message. Its example honeypot is fax_number; change the name if your form uses another. If CAPTCHA is enabled, integrate the configured widget and its response token before submitting.

3. Create ContactForm.jsx

The example keeps text fields in React state, snapshots the form before the request, and clears fields only after a confirmed success. A ref blocks repeated submits immediately while state controls the disabled UI. Use FormData with Accept: application/json; leave Content-Type to the browser. The start timestamp is initialized after mounting, avoiding different server/client render values. Save this component as ContactForm.jsx.

import { useEffect, useRef, useState } from 'react';
import './form-helper.css';

export default function ContactForm({ endpoint }) {
  const [fields, setFields] = useState({ name: '', email: '', message: '' });
  const [sending, setSending] = useState(false);
  const [feedback, setFeedback] = useState('');
  const inFlight = useRef(false);
  const startedAt = useRef(0);
  useEffect(() => { startedAt.current = Math.floor(Date.now() / 1000); }, []);

  function change(event) {
    const { name, value } = event.target;
    setFields((current) => ({ ...current, [name]: value }));
  }

  async function submit(event) {
    event.preventDefault();
    if (inFlight.current) return;
    const form = event.currentTarget;
    const body = new FormData(form);
    body.set('_cf_started_at', String(startedAt.current));
    inFlight.current = true;
    setSending(true);
    setFeedback('Sending…');
    try {
      const response = await fetch(endpoint, {
        method: 'POST',
        headers: { Accept: 'application/json' },
        body,
      });
      const result = await response.json().catch(() => null);
      if (!response.ok || result?.ok !== true) {
        const retry = response.headers.get('Retry-After');
        setFeedback((result?.error?.message || `Could not confirm receipt (HTTP ${response.status}).`)
          + (response.status === 429 && retry ? ` Retry after ${retry} seconds.` : ''));
        return;
      }
      setFeedback('Your submission was received.');
      form.reset();
      setFields({ name: '', email: '', message: '' });
      startedAt.current = Math.floor(Date.now() / 1000);
    } catch {
      setFeedback('We could not confirm receipt. Check before submitting again.');
    } finally {
      inFlight.current = false;
      setSending(false);
    }
  }

  return (
    <form action={endpoint} method="post" onSubmit={submit} aria-busy={sending}>
      <fieldset disabled={sending}>
        <legend>Contact us</legend>
        <label>Name <input name="name" value={fields.name} onChange={change} required /></label>
        <label>Email <input name="email" type="email" value={fields.email} onChange={change} required /></label>
        <label>Message <textarea name="message" value={fields.message} onChange={change} required /></label>
        <input className="form-helper" name="fax_number" type="text"
          tabIndex={-1} autoComplete="off" aria-hidden="true" />
        <button type="submit">{sending ? 'Sending…' : 'Send'}</button>
      </fieldset>
      <p role="status" aria-live="polite">{feedback}</p>
    </form>
  );
}

4. Render the form

Pass your endpoint as a prop from the parent component. Its cf_pk_ public key is expected to be visible in browser code. Do not pass a read key, management key, AWS credentials, or a webhook secret. The endpoint below is a placeholder and must be replaced.

import ContactForm from './ContactForm';

export default function App() {
  return <ContactForm endpoint="https://api.qubeforms.io/v1/cf_pk_YOUR_KEY/submissions" />;
}

5. Configure a Vite project

If your React app uses Vite, you can put the public endpoint in the customer project’s .env.local as VITE_QUBEFORMS_ENDPOINT and pass import.meta.env.VITE_QUBEFORMS_ENDPOINT as the prop. Vite exposes VITE_ values to browser code and replaces them during the build. Restart the development server after changing local environment values and set production values before building. These variables belong to your React project, not the Qubeforms service’s environment file.

VITE_QUBEFORMS_ENDPOINT=https://api.qubeforms.io/v1/cf_pk_YOUR_KEY/submissions

6. Handle success and redirects

Qubeforms normally returns HTTP 201 with ok: true, submission_id, status, trust_grade, redirect_url, and warnings. The component checks the HTTP result and ok before resetting. Receipt does not guarantee email delivery or an Inbox classification. The example displays an inline confirmation. For a thank-you page, configure the redirect allowlist, submit _cf_redirect, and explicitly navigate to the returned redirect_url after success. Do not put submitted private values into the redirect URL.

7. Extend and test the component

Add fields with a name, update the controlled state and reset values, and keep validation messages accessible. Do not use an effect to send a submission when state changes: submit in the event handler. Test a valid submission, offline mode, an invalid origin, and server validation failure. React state is lost when the component unmounts; see Multi-step forms in React for saved progress and Handling errors in React for retry behavior.

Keep private credentials private

Public form keys can appear in browser code. Read keys, management credentials, and webhook signing secrets must stay on the server.