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

@formos/react

v0.1.0

Published

React bindings for Formos - Headless form engine hooks

Readme

@formos/react

React bindings for the Formos form engine.

Purpose

@formos/react provides React hooks and components to integrate the headless @formos/kernel with React applications. It bridges the gap between the framework-agnostic form engine and React's component model.

Responsibilities

This package is responsible for:

  • React Context Management: Providing form engine instance via React context
  • Hook API: Exposing form functionality through idiomatic React hooks
  • State Synchronization: Keeping React components in sync with form state
  • SSR Compatibility: Safe server-side rendering support
  • Performance Optimization: Minimizing unnecessary re-renders

What This Package Does NOT Do

NO UI Components

  • Does not provide input components, buttons, or form elements
  • Does not include styling or CSS
  • Does not use shadcn/ui or any UI library
  • Headless - bring your own UI

NO Form Logic

  • Does not contain validation logic (use kernel adapters)
  • Does not define schemas (use @formos/schema)
  • Does not execute effects or validators
  • Pure React bindings only

NO Styling or Theming

  • No CSS or style objects
  • No theme providers
  • Style your components however you want

Think of it as: React hooks for the form engine, not a complete form library.

Stability Guarantee

Hooks API is STABLE ✅

The public hooks and components are committed to backward compatibility:

  • Hook signatures won't change
  • Return types remain consistent
  • Context API is stable

This enables:

  • Safe updates: Upgrade without breaking UI
  • Custom UI flexibility: Build any UI on these hooks
  • Long-term reliability: Code written today works tomorrow

Installation

pnpm add @formos/react @formos/kernel @formos/schema react

Basic Usage

1. Setup FormProvider

Wrap your application with FormProvider:

import { FormProvider } from '@formos/react';
import { normalizeSchema, type FormSchemaV1 } from '@formos/schema';

const schema: FormSchemaV1 = {
  version: 'v1',
  fields: [
    { 
      name: 'email', 
      type: 'email',
      required: true 
    },
    { 
      name: 'password', 
      type: 'password',
      required: true 
    }
  ]
};

const normalized = normalizeSchema(schema);

function App() {
  return (
    <FormProvider
      schema={normalized}
      validators={{
        required: (value) => value ? null : 'Required',
        email: (value) => {
          const regex = /\S+@\S+\.\S+/;
          return regex.test(String(value)) ? null : 'Invalid email';
        }
      }}
      onSubmit={async (values) => {
        console.log('Form submitted:', values);
        await fetch('/api/submit', {
          method: 'POST',
          body: JSON.stringify(values)
        });
      }}
    >
      <LoginForm />
    </FormProvider>
  );
}

2. Use Form Hooks

useForm - Form-level operations

import { useForm } from '@formos/react';

function LoginForm() {
  const { submit, reset, isValid, isDirty } = useForm();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const success = await submit();
    
    if (success) {
      console.log('Login successful');
    } else {
      console.log('Validation failed');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <EmailField />
      <PasswordField />
      
      <button type="submit" disabled={!isValid() || !isDirty()}>
        Login
      </button>
      <button type="button" onClick={reset}>
        Reset
      </button>
    </form>
  );
}

useField - Field-level state and actions

import { useField } from '@formos/react';

function EmailField() {
  const {
    value,
    error,
    touched,
    dirty,
    setValue,
    markTouched
  } = useField('email', {
    validateOnChange: true,
    validateOnBlur: true
  });

  return (
    <div>
      <label htmlFor="email">Email</label>
      <input
        id="email"
        type="email"
        value={String(value || '')}
        onChange={(e) => setValue(e.target.value)}
        onBlur={() => markTouched()}
        className={touched && error ? 'error' : ''}
      />
      {touched && error && (
        <span className="error-message">{error}</span>
      )}
      {dirty && <span className="dirty-indicator">*</span>}
    </div>
  );
}

useFormState - Observe form state

import { useFormState } from '@formos/react';

function SubmitButton() {
  const { isValid, isDirty, isSubmitting } = useFormState();

  return (
    <button
      type="submit"
      disabled={!isValid || !isDirty || isSubmitting}
    >
      {isSubmitting ? 'Submitting...' : 'Submit Form'}
    </button>
  );
}

function ErrorSummary() {
  const { errors } = useFormState();
  const errorList = Object.entries(errors).filter(([_, error]) => error);

  if (errorList.length === 0) return null;

  return (
    <div className="error-summary">
      <h3>Please fix the following errors:</h3>
      <ul>
        {errorList.map(([field, error]) => (
          <li key={field}>{field}: {error}</li>
        ))}
      </ul>
    </div>
  );
}

Multi-Step Forms

Use useStep for multi-step form navigation:

import { useStep } from '@formos/react';

const multiStepSchema: FormSchemaV1 = {
  version: 'v1',
  fields: [
    { name: 'firstName', type: 'text' },
    { name: 'lastName', type: 'text' },
    { name: 'email', type: 'email' },
    { name: 'phone', type: 'tel' }
  ],
  steps: [
    { id: 'personal', title: 'Personal Info', fields: ['firstName', 'lastName'] },
    { id: 'contact', title: 'Contact', fields: ['email', 'phone'] }
  ]
};

function StepNavigator() {
  const {
    currentStep,
    totalSteps,
    nextStep,
    prevStep,
    canGoToStep
  } = useStep();

  const handleNext = async () => {
    const moved = await nextStep();
    if (!moved) {
      alert('Please fix errors before proceeding');
    }
  };

  return (
    <div>
      <div className="step-indicator">
        Step {currentStep + 1} of {totalSteps}
      </div>
      
      <div className="step-content">
        {currentStep === 0 ? <PersonalInfoStep /> : <ContactStep />}
      </div>
      
      <div className="step-navigation">
        <button
          onClick={prevStep}
          disabled={currentStep === 0}
        >
          Previous
        </button>
        
        <button onClick={handleNext}>
          {currentStep === totalSteps - 1 ? 'Submit' : 'Next'}
        </button>
      </div>
    </div>
  );
}

Advanced Patterns

Custom Field Components

Create reusable field components:

interface TextFieldProps {
  name: string;
  label: string;
  type?: 'text' | 'email' | 'password' | 'tel';
}

function TextField({ name, label, type = 'text' }: TextFieldProps) {
  const { value, error, touched, setValue, markTouched } = useField(name);

  return (
    <div className="field">
      <label htmlFor={name}>{label}</label>
      <input
        id={name}
        type={type}
        value={String(value || '')}
        onChange={(e) => setValue(e.target.value)}
        onBlur={() => markTouched()}
        aria-invalid={touched && !!error}
        aria-describedby={error ? `${name}-error` : undefined}
      />
      {touched && error && (
        <span id={`${name}-error`} className="error" role="alert">
          {error}
        </span>
      )}
    </div>
  );
}

// Usage
<TextField name="email" label="Email Address" type="email" />

Conditional Fields

Show/hide fields based on conditions:

import { isFieldVisible } from '@formos/kernel';

function ConditionalField({ fieldName }: { fieldName: string }) {
  const { schema, engine } = useForm();
  const field = schema.fieldMap?.get(fieldName);
  
  if (!field || !isFieldVisible(field, (name) => engine.getValue(name))) {
    return null;
  }
  
  return <TextField name={fieldName} label={field.label || fieldName} />;
}

Form-level Validation

Trigger validation at form level:

function ValidateAllButton() {
  const { validate, getErrors } = useForm();

  const handleValidate = async () => {
    const isValid = await validate('manual');
    
    if (isValid) {
      alert('Form is valid!');
    } else {
      const errors = getErrors();
      console.log('Validation errors:', errors);
    }
  };

  return (
    <button type="button" onClick={handleValidate}>
      Validate All Fields
    </button>
  );
}

SSR (Server-Side Rendering)

The package is SSR-safe. Use it with Next.js, Remix, or other SSR frameworks:

// app/page.tsx (Next.js App Router)
'use client';

import { FormProvider, useField } from '@formos/react';
import { normalizeSchema } from '@formos/schema';

const schema = normalizeSchema({
  version: 'v1',
  fields: [{ name: 'email', type: 'email' }]
});

export default function ContactForm() {
  return (
    <FormProvider schema={schema} validators={{/* ... */}}>
      <EmailField />
    </FormProvider>
  );
}

Note: FormProvider must be used in a Client Component ('use client') because it manages React state.

Performance Optimization

Minimize Re-renders

The hooks use React context with a version counter system. Components only re-render when:

  1. Their specific field value changes (for useField)
  2. Form state changes (for useFormState)
  3. Step changes (for useStep)

Memoization

Form engine instance is memoized and only recreated if schema or validators change:

// Engine is stable across re-renders
const engine = useMemo(() => createFormEngine(schema, options), [schema, options]);

Selective Subscriptions

Only subscribe to the state you need:

// ❌ Bad - subscribes to all form state
function SubmitButton() {
  const { values, errors, isDirty } = useFormState();
  return <button disabled={!isDirty}>Submit</button>;
}

// ✅ Good - only checks dirty state when needed
function SubmitButton() {
  const { isDirty } = useForm();
  return <button disabled={!isDirty()}>Submit</button>;
}

API Reference

Components

  • FormProvider: Context provider for form engine

Hooks

  • useForm(): Form-level operations and engine access
  • useField(name, options?): Field-level state and actions
  • useFormState(): Reactive form state observation
  • useStep(): Multi-step navigation (multi-step forms only)

Types

  • FormProviderProps: Props for FormProvider
  • UseFieldOptions: Options for useField
  • UseFieldResult: Return type of useField
  • UseFormStateResult: Return type of useFormState
  • UseStepResult: Return type of useStep

TypeScript Support

Full TypeScript support with strict typing:

import type { UseFieldResult, UseFormStateResult } from '@formos/react';

// Field result is fully typed
const field: UseFieldResult = useField('email');

// Form state is fully typed
const state: UseFormStateResult = useFormState();

Package Structure

@formos/react/
├── src/
│   ├── index.ts           # Public API exports
│   ├── FormContext.tsx    # React context
│   ├── FormProvider.tsx   # Provider component
│   ├── useForm.ts         # Form hook
│   ├── useField.ts        # Field hook
│   ├── useFormState.ts    # State observation hook
│   ├── useStep.ts         # Multi-step hook
│   └── types.ts           # Type definitions
├── package.json
├── tsconfig.json
├── tsup.config.ts
└── README.md

Contributing

This package is part of the Formos monorepo. See the root README for contribution guidelines.

License

MIT