npm package: @formos/react
Description: React bindings for Formos - Headless form engine hooks
Latest version: 0.1.0

# @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

```bash
pnpm add @formos/react @formos/kernel @formos/schema react
```

## Basic Usage

### 1. Setup FormProvider

Wrap your application with `FormProvider`:

```tsx
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

```tsx
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

```tsx
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

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
// 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:

```tsx
// Engine is stable across re-renders
const engine = useMemo(() => createFormEngine(schema, options), [schema, options]);
```

### Selective Subscriptions

Only subscribe to the state you need:

```tsx
// ❌ 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:

```tsx
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