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

@wordpuppi/anyform-react

v0.5.1

Published

React hooks for anyform - headless form state, validation, and multi-step navigation

Readme

@wordpuppi/anyform-react

React hooks for anyform — headless form state, validation, and multi-step navigation.

Installation

npm install @wordpuppi/anyform-react

That's it! No bundler configuration needed. The default JS engine works everywhere.

Optional: Enable WASM Engine

For faster validation on large forms, install the optional WASM package:

npm install @wordpuppi/anyform-wasm-js
const form = useAnyForm('contact', { engine: 'wasm' });

See Bundler Configuration for WASM setup.

Quick Start

import { useAnyForm, AutoFormField } from '@wordpuppi/anyform-react';

function ContactForm() {
  const form = useAnyForm('contact', {
    baseUrl: 'http://localhost:3000',
    onSuccess: (result) => console.log('Submitted!', result),
  });

  if (form.isLoading) return <div>Loading...</div>;
  if (form.error) return <div>Error: {form.error}</div>;

  return (
    <form {...form.getFormProps()}>
      {form.visibleFields.map((field) => (
        <AutoFormField key={field.name} field={field} form={form} />
      ))}
      <button type="submit" disabled={form.isSubmitting}>
        {form.isSubmitting ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
}

Features

  • Zero Config — Works out of the box with pure JS engine
  • Headless — No styles, bring your own UI
  • Auto Field Rendering<AutoFormField /> handles all field types
  • Multi-step forms — Built-in step navigation
  • Conditional fields — Show/hide based on other field values
  • Optional WASM — Faster validation with @wordpuppi/anyform-wasm-js
  • Tailwind optional — Enable with tailwind: true
  • TypeScript — Full type definitions included

AutoFormField

The <AutoFormField /> component automatically renders the correct input type based on your field's field_type. No more switch statements!

import { useAnyForm, AutoFormField } from '@wordpuppi/anyform-react';

function MyForm() {
  const form = useAnyForm('my-form', { baseUrl: '...' });

  return (
    <form {...form.getFormProps()}>
      {form.visibleFields.map((field) => (
        <AutoFormField
          key={field.name}
          field={field}
          form={form}
          className="mb-4"
          errorClassName="text-red-500 text-sm"
        />
      ))}
      <button type="submit">Submit</button>
    </form>
  );
}

Props

| Prop | Type | Description | |------|------|-------------| | field | FieldJson | Field definition from form.visibleFields | | form | UseAnyFormReturn | Form state from useAnyForm | | className | string | Class for wrapper div | | errorClassName | string | Class for error messages (default: text-red-500 text-sm) | | renderField | (field, form) => ReactNode | Custom render override |

Custom Field Rendering

Override specific fields while using AutoFormField for the rest:

{form.visibleFields.map((field) => (
  <AutoFormField
    key={field.name}
    field={field}
    form={form}
    renderField={
      field.name === 'custom_field'
        ? (f, form) => <MyCustomInput field={f} form={form} />
        : undefined
    }
  />
))}

API Reference

useAnyForm(slug, options?)

Main hook for form state management.

const form = useAnyForm('my-form', {
  baseUrl: 'http://localhost:3000',  // API base URL
  engine: 'js',                       // 'js' (default) or 'wasm'
  tailwind: true,                     // Enable Tailwind classes
  initialValues: { email: '' },       // Pre-fill values
  initialSchema: schema,              // SSR hydration (skip fetch)
  validateOnChange: true,             // Validate on change (default: true)
  validateOnBlur: true,               // Validate on blur (default: true)
  onSubmit: async (values) => {},     // Custom submission handler
  onSuccess: (result) => {},          // Success callback
  onError: (error) => {},             // Error callback
});

Return Value

State

| Property | Type | Description | |----------|------|-------------| | schema | FormJson \| null | Form schema from server | | values | Record<string, unknown> | Current form values | | errors | Record<string, string[]> | Validation errors by field | | touched | Record<string, boolean> | Fields that have been interacted with | | isValid | boolean | Form validity (all visible fields valid) | | isLoading | boolean | Initial fetch in progress | | isSubmitting | boolean | Submission in progress | | isSubmitted | boolean | Successfully submitted | | error | string \| null | Error message from fetch or submission | | step | StepState \| null | Multi-step navigation state (null if single-step) |

Actions

| Method | Description | |--------|-------------| | setValue(field, value) | Set a field value | | setValues(values) | Set multiple values at once | | setTouched(field) | Mark a field as touched | | validateField(field) | Validate a single field, returns errors | | validateAll() | Validate all visible fields | | nextStep() | Navigate to next step (validates current step first) | | prevStep() | Navigate to previous step | | goToStep(stepId) | Go to specific step by ID | | submit() | Submit the form | | reset() | Reset form to initial state |

Props Helpers

| Method | Returns | Description | |--------|---------|-------------| | getFormProps() | FormProps | Props for <form> element | | getFieldProps(name) | FieldProps | Props for <input> element | | getSelectProps(name) | SelectProps | Props for <select> element | | getCheckboxProps(name) | CheckboxProps | Props for checkbox <input> | | getRadioGroupProps(name) | RadioGroupProps | Props for radio button group | | getTextareaProps(name) | TextareaProps | Props for <textarea> element | | getLabelProps(name) | LabelProps | Props for <label> element | | getFieldMeta(name) | FieldMeta | Field metadata (value, errors, touched, etc.) | | getStepProps() | StepProps | Props for step navigation buttons |

Visibility Helpers

| Property/Method | Description | |-----------------|-------------| | visibleFields | Array of visible fields for current step | | visibleSteps | Array of visible steps | | isFieldVisible(name) | Check if a field is visible | | isStepVisible(stepId) | Check if a step is visible |

Manual Field Rendering

If you prefer full control over field rendering, use the props helpers directly:

Text Input

<input {...form.getFieldProps('email')} />

Select

const selectProps = form.getSelectProps('country');

<select {...selectProps}>
  <option value="">Select...</option>
  {selectProps.options.map((opt) => (
    <option key={opt.value} value={opt.value}>
      {opt.label}
    </option>
  ))}
</select>

Multi-Select

const selectProps = form.getSelectProps('interests'); // multiple={true} automatically set

<select {...selectProps}>
  {selectProps.options.map((opt) => (
    <option key={opt.value} value={opt.value}>
      {opt.label}
    </option>
  ))}
</select>

Checkbox

<input {...form.getCheckboxProps('agree_terms')} />

Radio Group

const radioProps = form.getRadioGroupProps('plan');

<fieldset>
  {radioProps.options.map((opt) => (
    <label key={opt.value}>
      <input {...radioProps.getOptionProps(opt)} />
      {opt.label}
    </label>
  ))}
</fieldset>

Textarea

<textarea {...form.getTextareaProps('message')} />

Multi-Step Forms

function WizardForm() {
  const form = useAnyForm('onboarding');

  if (!form.step) return <div>Single step form</div>;

  return (
    <form {...form.getFormProps()}>
      <div>Step {form.step.progress[0]} of {form.step.progress[1]}</div>

      {form.visibleFields.map((field) => (
        <AutoFormField key={field.name} field={field} form={form} />
      ))}

      <div>
        {form.step.canGoPrev && (
          <button type="button" onClick={form.prevStep}>Back</button>
        )}
        {form.step.isLast ? (
          <button type="submit">Submit</button>
        ) : (
          <button type="button" onClick={form.nextStep}>Next</button>
        )}
      </div>
    </form>
  );
}

Context Provider

For app-wide configuration:

import { AnyFormProvider } from '@wordpuppi/anyform-react';

function App() {
  return (
    <AnyFormProvider baseUrl="https://api.example.com" tailwind>
      <ContactForm />
      <FeedbackForm />
    </AnyFormProvider>
  );
}

Render Props Component

Alternative to the hook:

import { AnyForm } from '@wordpuppi/anyform-react';

<AnyForm slug="contact" options={{ tailwind: true }}>
  {(form) => (
    <form {...form.getFormProps()}>
      {/* Your form UI */}
    </form>
  )}
</AnyForm>

Tailwind Integration

When tailwind: true is set, props helpers include sensible default classes:

form.getFieldProps('email')
// className: "block w-full rounded-md border-gray-300 shadow-sm ..."

// With errors:
// className: "block w-full rounded-md border-red-300 text-red-900 ..."

Customize with classNames option:

useAnyForm('contact', {
  tailwind: true,
  classNames: {
    input: 'my-custom-input',
    inputError: 'my-custom-error',
    label: 'my-custom-label',
  },
});

TypeScript

Full type definitions are included. Key types:

import type {
  FormJson,
  FieldJson,
  StepJson,
  UseAnyFormOptions,
  UseAnyFormReturn,
  FieldMeta,
  AutoFormFieldProps,
} from '@wordpuppi/anyform-react';

Troubleshooting

Form not loading / infinite loading

  1. Check that baseUrl is correct
  2. Verify the API is running: curl {baseUrl}/api/forms/{slug}/json
  3. Check browser console for CORS errors

"Failed to load WASM module" (only with engine: 'wasm')

This only affects WASM users. The default JS engine requires no config.

Solutions:

  1. Vite: Install vite-plugin-wasm (see Bundler Configuration)
  2. Next.js: Add asyncWebAssembly: true to webpack config
  3. CRA: Use craco (see Bundler Configuration)

Or just use the default JS engine (no config needed).

Validation errors not showing

  1. Ensure form.isLoading is false before interacting
  2. Check that field is touched: form.touched[fieldName]
  3. Validation runs on blur/change by default

Bundler Configuration (WASM Only)

Only needed if using engine: 'wasm'. The default JS engine works without any configuration.

Vite

// vite.config.ts
import { defineConfig } from 'vite';
import wasm from 'vite-plugin-wasm';

export default defineConfig({
  plugins: [wasm()],
  optimizeDeps: {
    exclude: ['@wordpuppi/anyform-wasm-js'],
  },
});

Install the plugin: npm install -D vite-plugin-wasm

Next.js

// next.config.js
module.exports = {
  webpack: (config) => {
    config.experiments = {
      ...config.experiments,
      asyncWebAssembly: true,
    };
    return config;
  },
};

Or use @wordpuppi/anyform-next which handles this automatically.

Webpack 5

// webpack.config.js
module.exports = {
  experiments: {
    asyncWebAssembly: true,
  },
};

Create React App (CRA)

CRA doesn't support WASM out of the box. Use craco:

npm install @craco/craco
// craco.config.js
module.exports = {
  webpack: {
    configure: (config) => {
      config.experiments = { asyncWebAssembly: true };
      return config;
    },
  },
};

Update package.json scripts to use craco instead of react-scripts.

License

MIT