React Forms
@snipform/react runs a SnipForm form entirely from React state. Your markup, your styling, SnipForm’s backend: validation, spam protection, double-submit protection and Signals tracking all happen on the server. Nothing in the package touches the DOM.
npm install @snipform/reactRequires React 18 or newer.
A complete form
Section titled “A complete form”import { useSnipForm, SnipForm, FieldError } from '@snipform/react';
export function ContactForm() { const form = useSnipForm({ key: 'YOUR_FORM_KEY', fields: { name: { type: 'text', rules: { required: 'Tell us your name' } }, email: { type: 'email', rules: { required: 'Email is required', email: null } }, message: { type: 'textarea', rules: { required: null, 'max_length[2000]': null } }, }, });
if (form.fatal) return <p>{form.fatal}</p>; if (form.success) return <div dangerouslySetInnerHTML={{ __html: form.success.html }} />;
return ( <SnipForm form={form}> <input {...form.register('name')} placeholder="Name" /> <FieldError form={form} name="name" />
<input {...form.register('email')} placeholder="Email" /> <FieldError form={form} name="email" />
<textarea {...form.register('message')} /> <FieldError form={form} name="message" />
<button disabled={form.isSubmitting}>Send</button> </SnipForm> );}That is the whole thing. The form key comes from the form’s page in the dashboard. Each piece:
| Piece | What it does |
|---|---|
useSnipForm(options) | Owns values, errors, status and the server session. Reference |
fields | Every field the form will submit, with its type and rules. The server validates exactly this set. |
<SnipForm form={form}> | A <form> with the hook’s handlers applied and the honeypot rendered. |
form.register('name') | Props for one input: value, onChange, onFocus, onBlur, onKeyDown, aria-invalid. Inputs |
<FieldError form={form} name="name" /> | The first server message for a field, with role="alert", or nothing. |
form.fatal / form.success | The two terminal states. States |
Why not the script tag?
Section titled “Why not the script tag?”The HTML library scans the DOM for sf-validate: directives and mutates elements directly: class names, text, innerHTML. React owns those elements and overwrites the changes on its next render, so error states flicker or disappear. This package keeps everything in React state and renders nothing itself beyond the optional components above.
How it works
Section titled “How it works”-
Idle until a human shows up. No request leaves until the visitor focuses, clicks, types, touches, or scrolls the form into view. That first interaction opens a server session (
initializing, thenready). -
The field set is sent and frozen.
fieldsgoes to the server at init. From then on the server validates exactly those fields and refuses anything else, so declare every field the form can ever submit, including ones you render conditionally. -
Validation is the server’s. The same rules the dashboard documents, run by the backend. Nothing is duplicated client-side, so client and server cannot disagree. Validation
-
Submit posts the values, the empty honeypot and the behavioural signals the spam scorer reads. Success returns the form’s thank-you HTML with
%field%variables filled in. A validation failure returns every message per field and keeps the session open for another try.
The session mechanics (signatures, the 600 second TTL, single use, IP pinning) are shared with the other clients and described in How Forms Work.
Frameworks
Section titled “Frameworks”The hook uses state, effects and fetch, so the component must be a client component. It is safe to render on the server: nothing is requested until a browser interaction happens.
'use client';
import { useSnipForm, SnipForm, FieldError } from '@snipform/react';
export function ContactForm() { const form = useSnipForm({ key: 'YOUR_FORM_KEY', fields: { email: { type: 'email', rules: { required: null, email: null } } } }); // ...}Nothing extra. Import and render.
import { ContactForm } from './ContactForm';
export default function App() { return <ContactForm />;}Render the component client-side (client:load or client:visible in Astro). Server rendering is fine; the session opens only once the island hydrates and the visitor interacts.
Developing on localhost
Section titled “Developing on localhost”The server checks the request’s Referer against the property’s domain. For a localhost or 127.0.0.1 dev server, switch on the form’s localhost toggle in the dashboard. Other hostnames (0.0.0.0, *.local, LAN IPs) are not covered; use localhost.
- useSnipForm - every option, and everything on the handle.
- Inputs - text, select, checkbox groups, radios and hidden fields.
- Validation - rules, messages and validate-on-blur.
- States and success - render per status, show the thank-you, handle errors.