npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@formflowjs/react

v1.0.3

Published

Headless React adapter for FormFlow: hooks, providers, and renderless components that bind your own markup to the framework-agnostic @formflowjs/core engine.

Readme

@formflowjs/react

Headless React adapter for FormFlow — the @formflowjs/strapi-plugin-formflow Strapi plugin. Fetch a form schema by slug and render it with your own markup and styles (Tailwind, shadcn, MUI, plain CSS). The SDK owns schema typing, client-side validation (parity with the server), conditional visibility, multi-step flow, file uploads, and captcha/honeypot plumbing. It ships no CSS and renders no markup of its own.

  • Headless — every element is yours; the SDK hands you value + errors + ARIA-complete prop getters.
  • SSR / RSC-safeuseSyncExternalStore with a server snapshot, useId for ids, no top-level browser globals. A "use client" banner is baked into the build for the Next.js App Router.
  • Typed — full TypeScript types for the public content-API contract.
npm i @formflowjs/react @formflowjs/core
# peer deps: react >= 18, react-dom >= 18

Core concepts

| Export | Purpose | | --- | --- | | <FormFlowProvider form={schema} baseUrl="…"> | Creates and owns a reactive store (or accepts an external store). | | useFormFlow() | { schema, state, values, errors, isSubmitting, status, result, fields, stepFields, currentStep, stepCount, isFirstStep, isLastStep, setFieldValue, submit, reset, nextStep, prevStep, goToStep, savePartial, loadPartial, setCaptchaToken, getFormProps() } | | <FormFlowField name="…" render={…}> | Per-field binding; render prop or children-as-function. Exposes value/errors/error/invalid/touched/dirty/visible, setValue/setTouched, and prop getters. | | <FormFlowStep render={…}> | Multi-step controller: { index, total, fields, isFirst, isLast, getNextProps(), getPrevProps(), getSubmitProps() }. | | <FormFlowHoneypot /> | Renders the schema's honeypot input (visually hidden, aria-hidden, tabindex=-1, autocomplete off). |

FormFlowField prop getters

getInputProps(), getTextareaProps(), getSelectProps(), getCheckboxProps(optionValue?), getRadioProps(optionValue), getOptionProps(optionValue), getControlProps() (a role="group" wrapper for choice groups), getLabelProps(), getErrorProps(), getDescriptionProps(), getFileProps().

Each getter accepts an overrides object merged last, and wires full ARIA (aria-invalid, aria-required, aria-describedby) plus data-invalid / data-touched / data-dirty / data-required so you can style purely from state — no class toggling needed.

Fetch the schema

@formflowjs/core ships a tiny content-API client:

import { createFormFlowClient } from '@formflowjs/core';

const client = createFormFlowClient({ baseUrl: 'https://cms.example.com' });
const schema = await client.getForm('contact-form'); // FormSchema

You can pass client to the provider via options={{ client }}, or just pass baseUrl and let the provider create one.


Next.js (App Router)

The form is interactive, so it lives in a Client Component. Fetch the schema in a Server Component and hand it down.

// app/contact/page.tsx  (Server Component)
import { createFormFlowClient } from '@formflowjs/core';
import { ContactForm } from './contact-form';

export default async function Page() {
  const client = createFormFlowClient({ baseUrl: process.env.CMS_URL! });
  const schema = await client.getForm('contact-form');
  return <ContactForm schema={schema} />;
}
// app/contact/contact-form.tsx
'use client';

import {
  FormFlowProvider,
  FormFlowField,
  FormFlowHoneypot,
  useFormFlow,
  type FormSchema,
} from '@formflowjs/react';

function Fields() {
  const f = useFormFlow();
  return (
    <form {...f.getFormProps()}>
      <FormFlowHoneypot />
      {f.fields.map((field) => (
        <FormFlowField
          key={field.name}
          name={field.name}
          render={(api) => (
            <p>
              <label {...api.getLabelProps()}>{api.field.label}</label>
              {field.type === 'textarea' ? (
                <textarea {...api.getTextareaProps()} />
              ) : (
                <input {...api.getInputProps()} />
              )}
              {api.invalid && <span {...api.getErrorProps()}>{api.error}</span>}
            </p>
          )}
        />
      ))}
      <button type="submit" disabled={f.isSubmitting}>
        {f.schema.settings.submitButtonText}
      </button>
      {f.status === 'success' && <p role="status">{f.result?.message}</p>}
    </form>
  );
}

export function ContactForm({ schema }: { schema: FormSchema }) {
  return (
    <FormFlowProvider form={schema} baseUrl={process.env.NEXT_PUBLIC_CMS_URL}>
      <Fields />
    </FormFlowProvider>
  );
}

The published bundle already carries a "use client" banner, but keep your own wrapper marked "use client" so the JSX that calls the hooks runs on the client.


Astro (client:load)

Render the React island with a client:* directive and pass the schema as a prop (fetch it in the .astro frontmatter).

---
// src/pages/contact.astro
import { createFormFlowClient } from '@formflowjs/core';
import ContactForm from '../components/ContactForm.tsx';

const client = createFormFlowClient({ baseUrl: import.meta.env.CMS_URL });
const schema = await client.getForm('contact-form');
---
<ContactForm schema={schema} client:load />
// src/components/ContactForm.tsx
import {
  FormFlowProvider,
  FormFlowField,
  FormFlowHoneypot,
  useFormFlow,
  type FormSchema,
} from '@formflowjs/react';

function Fields() {
  const f = useFormFlow();
  return (
    <form {...f.getFormProps()}>
      <FormFlowHoneypot />
      {f.fields.map((field) => (
        <FormFlowField key={field.name} name={field.name}>
          {(api) => (
            <label>
              {api.field.label}
              <input {...api.getInputProps()} />
              {api.invalid && <span {...api.getErrorProps()}>{api.error}</span>}
            </label>
          )}
        </FormFlowField>
      ))}
      <button type="submit">Submit</button>
    </form>
  );
}

export default function ContactForm({ schema }: { schema: FormSchema }) {
  return (
    <FormFlowProvider form={schema} baseUrl={import.meta.env.PUBLIC_CMS_URL}>
      <Fields />
    </FormFlowProvider>
  );
}

Use client:load for a form that must be interactive immediately, or client:visible to hydrate when it scrolls into view.


Vite + React

A plain client-side app: fetch on mount, then render.

// src/App.tsx
import { useEffect, useState } from 'react';
import {
  createFormFlowClient,
  type FormSchema,
} from '@formflowjs/core';
import {
  FormFlowProvider,
  FormFlowField,
  FormFlowHoneypot,
  useFormFlow,
} from '@formflowjs/react';

const client = createFormFlowClient({ baseUrl: import.meta.env.VITE_CMS_URL });

function Fields() {
  const f = useFormFlow();
  return (
    <form {...f.getFormProps()} className="space-y-4">
      <FormFlowHoneypot />
      {f.fields.map((field) => (
        <FormFlowField
          key={field.name}
          name={field.name}
          render={(api) => (
            <div className="flex flex-col gap-1">
              <label {...api.getLabelProps()} className="font-medium">
                {api.field.label}
              </label>
              <input
                {...api.getInputProps()}
                className="rounded border px-3 py-2 data-[invalid]:border-red-500"
              />
              {api.invalid && (
                <span {...api.getErrorProps()} className="text-sm text-red-600">
                  {api.error}
                </span>
              )}
            </div>
          )}
        />
      ))}
      <button
        type="submit"
        disabled={f.isSubmitting}
        className="rounded bg-black px-4 py-2 text-white"
      >
        {f.schema.settings.submitButtonText}
      </button>
      {f.status === 'success' && <p role="status">{f.result?.message}</p>}
    </form>
  );
}

export default function App() {
  const [schema, setSchema] = useState<FormSchema | null>(null);
  useEffect(() => {
    client.getForm('contact-form').then(setSchema);
  }, []);
  if (!schema) return <p>Loading…</p>;
  return (
    <FormFlowProvider form={schema} baseUrl={import.meta.env.VITE_CMS_URL}>
      <Fields />
    </FormFlowProvider>
  );
}

The data-[invalid]: / data-[touched]: Tailwind variants pair naturally with the data-* attributes the prop getters emit.


Multi-step forms

When the schema's settings.layout === 'multi-step', drive the wizard with <FormFlowStep>:

import { FormFlowStep, FormFlowField } from '@formflowjs/react';

<FormFlowStep
  render={({ fields, isFirst, isLast, getPrevProps, getNextProps, getSubmitProps }) => (
    <>
      {fields.map((field) => (
        <FormFlowField key={field.name} name={field.name} render={/* … */} />
      ))}
      <div>
        {!isFirst && <button {...getPrevProps()}>Back</button>}
        {isLast ? (
          <button {...getSubmitProps()}>Finish</button>
        ) : (
          <button {...getNextProps()}>Next</button>
        )}
      </div>
    </>
  )}
/>

getNextProps() validates the current step (client-side, or server-side when options.serverStepValidation is set) and advances only when valid.

Captcha & honeypot

The honeypot is rendered for you by <FormFlowHoneypot />. Captcha widgets stay your responsibility — render reCAPTCHA / Turnstile / hCaptcha yourself and feed the token into the store:

const f = useFormFlow();
// in the widget's onVerify callback:
f.setCaptchaToken('recaptcha', token);

Site keys: the public schema exposes the reCAPTCHA site key + version (schema.settings.spam.recaptcha). Turnstile/hCaptcha keys are never in the schema — pass them via options.captcha.

License

MIT