Skip to content

Add forms to React

Add a form backend to your React application. Forminit handles submissions, validation, file uploads, and notifications.

Build with AI

Paste this prompt into your AI coding tool or app builder. It points the agent to the Forminit setup guide, which wires your form and keeps your design. Your form's page in the dashboard has the same prompt with the Form ID filled in.

SETUP PROMPT

Connect my form to Forminit (forminit.com, a form backend). If I have no form yet, ask me which fields I want, then create it.

Form ID: ask me for it before writing code (it is on my form's page at app.forminit.com).

Get the Forminit setup guide, then follow it:
1. If you can run terminal commands, install it: npx skills add forminit/skills --skill forminit-setup -a universal claude-code -y. Then read .agents/skills/forminit-setup/SKILL.md.
2. Otherwise read https://forminit.com/setup.md in full.
3. If you can do neither, stop and tell me.


Submit forms directly from your React components — no backend required. Forminit validates and stores every submission for you.

  1. Create a Forminit account at forminit.com
  2. Create a form in your dashboard
  3. Set authentication mode to Public in Form Settings
# npm
npm install forminit

# yarn
yarn add forminit

# pnpm
pnpm add forminit

The example below packs every block type into a single component so you can see the full naming pattern in one place. Every input name follows fi-{blockType}-{name} — that’s the only rule. Most forms only need a handful of these, so keep the blocks you need and delete the rest.

import { useState, FormEvent } from 'react';
import { Forminit } from 'forminit';

const forminit = new Forminit();
const FORM_ID = 'YOUR_FORM_ID';

export function ContactForm() {
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
  const [error, setError] = useState<string | null>(null);

  async function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus('loading');
    setError(null);

    const form = e.currentTarget;
    const formData = new FormData(form);

    const { data, redirectUrl, error } = await forminit.submit(FORM_ID, formData);

    if (error) {
      setStatus('error');
      setError(error.message);
      return;
    }

    setStatus('success');
    form.reset();
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* sender: who's submitting (built-in, max 1 per submission) */}
      <input type="text"  name="fi-sender-firstName" placeholder="First name" />
      <input type="text"  name="fi-sender-lastName"  placeholder="Last name" />
      {/* Single-field alternative to firstName/lastName */}
      <input type="text"  name="fi-sender-fullName"  placeholder="Full name" />
      <input type="email" name="fi-sender-email"     placeholder="Email" required />
      <input type="text"  name="fi-sender-phone"     placeholder="Phone" />
      <input type="text"  name="fi-sender-title"     placeholder="Title (Mr., Ms., Dr.)" />
      <input type="text"  name="fi-sender-company"   placeholder="Company" />
      <input type="text"  name="fi-sender-position"  placeholder="Job title" />
      <input type="text"  name="fi-sender-address"   placeholder="Address" />
      <input type="text"  name="fi-sender-address2"  placeholder="Address line 2 (apt, suite, etc.)" />
      <input type="text"  name="fi-sender-city"      placeholder="City" />
      <input type="text"  name="fi-sender-postcode"  placeholder="Postcode / ZIP" />
      <input type="text"  name="fi-sender-country"   placeholder="Country (ISO 3166-1 alpha-2, e.g. US)" />
      <input type="hidden" name="fi-sender-userId"   value="ext_12345" />

      {/* text: free-form answer, no validation */}
      <textarea name="fi-text-message" placeholder="Message" />

      {/* number: any numeric value */}
      <input type="number" name="fi-number-budget" placeholder="Budget" />

      {/* email: a second email beyond the sender's */}
      <input type="email" name="fi-email-invitee" placeholder="Invitee email" />

      {/* phone: a second phone beyond the sender's, E.164 format */}
      <input type="text" name="fi-phone-emergency" placeholder="Emergency contact" />

      {/* url: validated as a URL */}
      <input type="url" name="fi-url-website" placeholder="Your website" />

      {/* select: single or multi choice */}
      <select name="fi-select-plan">
        <option value="basic">Basic</option>
        <option value="pro">Pro</option>
        <option value="enterprise">Enterprise</option>
      </select>

      {/* radio: single choice from a group */}
      <label><input type="radio" name="fi-radio-priority" value="low"  /> Low</label>
      <label><input type="radio" name="fi-radio-priority" value="high" /> High</label>

      {/* checkbox: one or more choices, same name = array */}
      <label><input type="checkbox" name="fi-checkbox-features" value="api" />     API</label>
      <label><input type="checkbox" name="fi-checkbox-features" value="support" /> Support</label>

      {/* rating: integer between 1 and 5 */}
      <input type="range" name="fi-rating-satisfaction" min={1} max={5} step={1} defaultValue={3} />

      {/* date: ISO 8601 date or date-time */}
      <input type="date" name="fi-date-appointment" />

      {/* file: requires multipart/form-data (FormData uses it by default) */}
      <input type="file" name="fi-file-resume" />

      {/* country: ISO 3166-1 alpha-2 code */}
      <select name="fi-country-shipping">
        <option value="US">United States</option>
        <option value="GB">United Kingdom</option>
      </select>

      {status === 'error' && <p className="error">{error}</p>}
      {status === 'success' && <p className="success">Message sent successfully!</p>}

      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Sending...' : 'Send'}
      </button>
    </form>
  );
}

For a full reference on each block type and its validation rules, see the Form Blocks documentation.


File uploads require FormData. Add file inputs with the fi-file-{name} naming pattern:

import { useState, useRef, FormEvent } from 'react';
import { Forminit } from 'forminit';

const forminit = new Forminit();
const FORM_ID = 'YOUR_FORM_ID';

export function ApplicationForm() {
  const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
  const [error, setError] = useState<string | null>(null);
  const formRef = useRef<HTMLFormElement>(null);

  async function handleSubmit(e: FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus('loading');
    setError(null);

    const formData = new FormData(e.currentTarget);
    const { data, error } = await forminit.submit(FORM_ID, formData);

    if (error) {
      setStatus('error');
      setError(error.message);
      return;
    }

    setStatus('success');
    formRef.current?.reset();
  }

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      <input
        type="text"
        name="fi-sender-firstName"
        placeholder="First name"
        required
      />
      <input
        type="text"
        name="fi-sender-lastName"
        placeholder="Last name"
        required
      />
      <input
        type="email"
        name="fi-sender-email"
        placeholder="Email"
        required
      />

      <label>Resume (PDF)</label>
      <input
        type="file"
        name="fi-file-resume"
        accept=".pdf,.doc,.docx"
        required
      />

      <label>Portfolio (optional, multiple files)</label>
      <input
        type="file"
        name="fi-file-portfolio[]"
        accept="image/*,.pdf"
        multiple
      />

      {status === 'error' && <p className="error">{error}</p>}
      {status === 'success' && <p className="success">Application submitted!</p>}

      <button type="submit" disabled={status === 'loading'}>
        {status === 'loading' ? 'Submitting...' : 'Submit Application'}
      </button>
    </form>
  );
}

Important: When using the multiple attribute, append [] to the field name (e.g., fi-file-portfolio[]).


The SDK returns { data, redirectUrl, error }. On successful submission:

data contains the submission details:

{
  "hashId": "7LMIBoYY74JOCp1k",
  "date": "2026-01-01 21:10:24",
  "blocks": {
    "sender": {
      "firstName": "John",
      "lastName": "Doe",
      "email": "john.doe@example.com"
    },
    "message": "Hello world"
  }
}

redirectUrl contains the thank you page URL (always returned).

Field Type Description
data.hashId string Unique submission identifier
data.date string Submission timestamp (YYYY-MM-DD HH:mm:ss)
data.blocks object All submitted field values
redirectUrl string Thank you page URL

Error Response:

{
  "error": "ERROR_CODE",
  "code": 400,
  "message": "Human-readable error message"
}

Every successful submission returns a redirectUrl. You configure this URL in Form Settings → Redirections — so you can point users to a different thank-you page anytime without changing your code or redeploying. Just read redirectUrl from the response and send the user there:

async function handleSubmit(e: FormEvent<HTMLFormElement>) {
  e.preventDefault();

  const formData = new FormData(e.currentTarget);
  const { data, redirectUrl, error } = await forminit.submit(FORM_ID, formData);

  if (error) {
    setStatus('error');
    setError(error.message);
    return;
  }

  // Redirect to the URL configured in your Forminit form settings
  window.location.href = redirectUrl;
}

If the redirect target is a route inside your own app, use your router for a client-side navigation instead (e.g. React Router’s useNavigate):

import { useNavigate } from 'react-router-dom';

const navigate = useNavigate();

// ...after a successful submit:
navigate(redirectUrl);

For complete documentation on all available blocks, field naming conventions, and validation rules, see the Form Blocks Reference.


Handle common errors appropriately:

async function handleSubmit(e: FormEvent<HTMLFormElement>) {
  e.preventDefault();
  setStatus('loading');
  setError(null);

  const formData = new FormData(e.currentTarget);
  const { data, error } = await forminit.submit(FORM_ID, formData);

  if (error) {
    setStatus('error');

    switch (error.error) {
      case 'FI_SCHEMA_FORMAT_EMAIL':
        setError('Please enter a valid email address.');
        break;
      case 'FI_RULES_PHONE_INVALID':
        setError('Please enter a valid phone number (e.g., +12025550123).');
        break;
      case 'FI_SCHEMA_RANGE_RATING':
        setError('Rating must be between 1 and 5.');
        break;
      case 'TOO_MANY_REQUESTS':
        setError('Please wait a moment before submitting again.');
        break;
      default:
        setError(error.message);
    }
    return;
  }

  setStatus('success');
  e.currentTarget.reset();
}
Error Code Description
FORM_NOT_FOUND Form ID doesn’t exist or was deleted
FORM_DISABLED Form is disabled by owner
EMPTY_SUBMISSION No fields with values submitted
FI_SCHEMA_FORMAT_EMAIL Invalid email format
FI_RULES_PHONE_INVALID Invalid phone number format
FI_SCHEMA_RANGE_RATING Rating not between 1-5
FI_DATA_COUNTRY_INVALID Invalid country code
TOO_MANY_REQUESTS Rate limit exceeded

  1. Validate input client-side — Provide immediate feedback to users
  2. Don’t rely solely on client validation — Forminit validates server-side automatically
  3. Restrict submissions with authorized domains — Limit which domains can submit to your form in Form Settings
  4. Add spam protection — Enable Cloudflare Turnstile, reCAPTCHA, hCaptcha, or honeypot for public forms
  5. Use HTTPS — Always use secure connections in production

View this page as Markdown