States & Success
Everything the form can be in is on form.status, and the two terminal states have their own fields: form.fatal and form.success. A typical component branches on those first, then renders the form.
if (form.fatal) return <FormUnavailable message={form.fatal} />;if (form.success) return <ThankYou result={form.success} onAgain={form.reset} />;
return ( <SnipForm form={form}> ... {form.error && <p role="alert">Something went wrong. Please try again.</p>} <button disabled={form.isSubmitting}>{form.isSubmitting ? 'Sending...' : 'Send'}</button> </SnipForm>);Success
Section titled “Success”form.success is { html, values }:
html | The form’s thank-you content from the dashboard, with %field% variables already substituted by the server. '' if the form has no thank-you content. |
values | The values that were submitted, keyed by field. |
Render the dashboard content as-is. It is HTML authored by you in the dashboard, not by the visitor, which is why dangerouslySetInnerHTML is acceptable here.
if (form.success) { return <div className="prose" dangerouslySetInnerHTML={{ __html: form.success.html }} />;}Ignore html and build the message from values.
if (form.success) { const { name, email } = form.success.values; return ( <div> <h2>Thanks, {name}.</h2> <p>We will reply to {email} within a day.</p> <button onClick={form.reset}>Send another</button> </div> );}onSuccess fires once with the same object, which is where a redirect or an analytics call belongs.
const router = useRouter();const form = useSnipForm({ key, fields, onSuccess: () => router.push('/thanks'),});A successful submit consumes the session: status is success, the honeypot disappears, and the form cannot be submitted again until reset().
reset()
Section titled “reset()”<button onClick={form.reset}>Start over</button>Values return to their initials, errors, success and error clear, the session is dropped and status goes back to idle. The next interaction opens a fresh session. Use it after a success, or to clear a form the visitor abandons.
form.fatal is a string when the form cannot run at all. It is set during init, so it usually appears at the first interaction.
| Cause | Message you will see |
|---|---|
Wrong key | [Key error] SnipForm not found. ... |
| Form not published in the dashboard | [Config error] SnipForm not published |
Page domain not registered for the form (or localhost without the toggle) | [Config error] example.com is not a registered domain for this form |
| A rule name the server does not know | [Config error] Unknown validation rule: "..."; |
| Request signature rejected at init (a badly skewed device clock) | [Lib Error] Signature failed |
status is fatal, onError fires with a SnipFormError whose kind is 'fatal', and submit() becomes a no-op. Fix the cause; there is nothing the visitor can do.
if (form.fatal) { return ( <p role="alert"> This form is temporarily unavailable. {process.env.NODE_ENV !== 'production' && <code> {form.fatal}</code>} </p> );}Recoverable errors
Section titled “Recoverable errors”form.error is a SnipFormError when an init or submit failed for a reason that might not happen again. status is error, onError fires, and the visitor can submit again.
error.kind | error.status | When | What the hook already did |
|---|---|---|---|
'network' | none | fetch threw: offline, CORS, DNS. | Nothing. Submitting again retries. If it happened at init, the next submit opens the session again. |
'expired' | 419 | The session expired (600 seconds), was already used, or the visitor’s connection changed. | Opened a new session and retried once. You only see this if the retry also failed. |
'unauthorized' | 403 | The request signature was rejected or the honeypot was filled. | Nothing. Usually means a badly skewed clock or a bot. |
'server' | other | Anything else non-2xx. | Nothing. |
{form.error && ( <p role="alert"> {form.error.kind === 'network' ? 'You look offline. Check your connection and try again.' : 'We could not send that. Please try again in a moment.'} </p>)}error clears at the start of the next submit.
Branding
Section titled “Branding”On plans that require it, form.branding is { label, link } once the session is open. Render it somewhere visible near the form:
import { Branding } from '@snipform/react';
<Branding form={form} className="text-xs text-gray-500" />It outputs <a href={link} target="_blank" rel="noreferrer">{label}</a>, or nothing when the plan does not need it.
Putting it together
Section titled “Putting it together”import { useSnipForm, SnipForm, FieldError, Branding } from '@snipform/react';
export function NewsletterForm() { const form = useSnipForm({ key: 'YOUR_FORM_KEY', fields: { email: { type: 'email', rules: { required: 'Email is required', email: null } }, }, validateOn: 'blur', });
if (form.fatal) { return <p role="alert">Signups are paused right now.</p>; }
if (form.success) { return ( <p> You are on the list, {form.success.values.email}.{' '} <button onClick={form.reset}>Add another</button> </p> ); }
return ( <SnipForm form={form} className="flex flex-col gap-2"> <FieldError form={form} name="email" /> {form.error && <p role="alert">Could not subscribe. Try again.</p>} <button disabled={form.isSubmitting}>{form.isSubmitting ? 'Joining...' : 'Join'}</button> <Branding form={form} /> </SnipForm> );}