Skip to content

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>
);

form.success is { html, values }:

htmlThe form’s thank-you content from the dashboard, with %field% variables already substituted by the server. '' if the form has no thank-you content.
valuesThe 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 }} />;
}

A successful submit consumes the session: status is success, the honeypot disappears, and the form cannot be submitted again until 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.

CauseMessage 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>
);
}

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.kinderror.statusWhenWhat the hook already did
'network'nonefetch threw: offline, CORS, DNS.Nothing. Submitting again retries. If it happened at init, the next submit opens the session again.
'expired'419The 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'403The request signature was rejected or the honeypot was filled.Nothing. Usually means a badly skewed clock or a bot.
'server'otherAnything 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.

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.

NewsletterForm.tsx
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">
<input {...form.register('email')} placeholder="[email protected]" />
<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>
);
}