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

@skyroc/form

v0.1.0

Published

Advanced form handling library with type-safe field management for React applications

Readme

Skyroc Form

English | 简体中文

A powerful, type-safe React form management library with complete form state management, validation, and dynamic field support.

✨ Features

  • 🎯 Full TypeScript Support - Built with TypeScript, providing complete type inference and type safety
  • 📦 Lightweight Design - Minimal core functionality without extra dependency burden
  • 🔄 Reactive State Management - Efficient form state subscription and update mechanism
  • Powerful Validation System - Supports sync/async validation, built-in rules, and custom validators
  • 🎨 Flexible Component Design - Supports polymorphic rendering and headless mode
  • 📝 Dynamic Array Fields - Complete array field management (CRUD, sorting, etc.)
  • 🔌 Schema Validation Support - Compatible with mainstream validation libraries like Zod, Yup, etc.
  • Performance Optimized - Precise field subscription to avoid unnecessary re-renders
  • 🔧 Middleware System - Supports extension and custom form behavior
  • ↩️ Undo/Redo - Built-in undo/redo functionality

📦 Installation

npm install skyroc-form
# or
pnpm add skyroc-form
# or
yarn add skyroc-form

🚀 Quick Start

Basic Form

import { Form, Field } from 'skyroc-form';

function BasicForm() {
  return (
    <Form
      initialValues={{ username: '', email: '' }}
      onFinish={values => {
        console.log('Form submitted:', values);
      }}
    >
      <Field name="username">
        <input placeholder="Username" />
      </Field>

      <Field name="email">
        <input
          type="email"
          placeholder="Email"
        />
      </Field>

      <button type="submit">Submit</button>
    </Form>
  );
}

Form with Validation

import { Form, Field, useForm } from 'skyroc-form';

function ValidatedForm() {
  const [form] = useForm();

  return (
    <Form
      form={form}
      initialValues={{ email: '', password: '' }}
      onFinish={values => console.log('Submit success:', values)}
      onFinishFailed={error => console.log('Validation failed:', error)}
    >
      <Field
        name="email"
        rules={[
          { required: true, message: 'Please enter email' },
          { type: 'email', message: 'Invalid email format' }
        ]}
      >
        <input placeholder="Email" />
      </Field>

      <Field
        name="password"
        rules={[
          { required: true, message: 'Please enter password' },
          { minLength: 6, message: 'Password must be at least 6 characters' }
        ]}
      >
        <input
          type="password"
          placeholder="Password"
        />
      </Field>

      <button type="submit">Submit</button>
      <button
        type="button"
        onClick={() => form.resetFields()}
      >
        Reset
      </button>
    </Form>
  );
}

📚 Core API

Components

<Form>

Form container component that provides form context and state management.

Props:

| Property | Type | Default | Description | | ------------------ | ------------------------ | ------------ | ------------------------------------------- | | form | FormInstance | - | External form instance | | initialValues | object | {} | Initial form values | | onFinish | (values) => void | - | Callback on successful submission | | onFinishFailed | (error) => void | - | Callback on failed submission | | onValuesChange | (changed, all) => void | - | Callback on value changes | | onFieldsChange | (changed, all) => void | - | Callback on field metadata changes | | schema | FormSchema | - | Schema validator (supports Zod, etc.) | | validateTrigger | string \| string[] | 'onChange' | Validation trigger event | | validateMessages | ValidateMessages | - | Custom validation messages | | preserve | boolean | true | Whether to preserve field values on unmount | | component | ElementType \| false | 'form' | Component type to render as |

Examples:

// Basic usage
<Form initialValues={{ name: 'John' }}>
  {/* Fields */}
</Form>

// Headless mode (no wrapper element)
<Form component={false}>
  <div className="custom-layout">
    {/* Fields */}
  </div>
</Form>

// Using Schema validation
const schema = z.object({
  username: z.string().min(3),
  age: z.number().min(18)
});

<Form schema={schema} onFinish={handleSubmit}>
  {/* Fields */}
</Form>

<Field>

Field component for wrapping input controls with state management and validation.

Props:

| Property | Type | Default | Description | | ------------------- | -------------------------------------- | ------------ | ---------------------------------------------------- | | name | string | Required | Field name (supports nested paths like user.email) | | rules | Rule[] | - | Array of validation rules | | initialValue | any | - | Initial field value | | trigger | string | 'onChange' | Event that triggers value update | | validateTrigger | string \| string[] | - | Event(s) that trigger validation | | valuePropName | string | 'value' | Name of the value prop | | getValueFromEvent | (...args) => any | - | Custom value extraction function | | normalize | (value, prevValue, allValues) => any | - | Value normalization/transformation function | | preserve | boolean | true | Whether to preserve value on unmount |

Examples:

// Basic field
<Field name="username">
  <input />
</Field>

// With validation rules
<Field
  name="phone"
  rules={[
    { required: true, message: 'Please enter phone number' },
    { pattern: /^1\d{10}$/, message: 'Invalid phone format' }
  ]}
>
  <input />
</Field>

// Custom value extraction and normalization
<Field
  name="phone"
  getValueFromEvent={(e) => e.target.value.replace(/\D/g, '')}
  normalize={(value) => {
    const cleaned = value.replace(/\D/g, '');
    const match = cleaned.match(/^(\d{3})(\d{4})(\d{4})$/);
    return match ? `${match[1]}-${match[2]}-${match[3]}` : value;
  }}
>
  <input placeholder="138-0013-8000" />
</Field>

// Nested field
<Field name="user.profile.email">
  <input type="email" />
</Field>

<List>

Dynamic array field management component with complete array operations.

Props:

| Property | Type | Default | Description | | -------------- | ----------------------------------- | ------------ | ------------------- | | name | string | Required | Array field name | | initialValue | any[] | - | Initial array value | | children | (fields, operations) => ReactNode | Required | Render function |

Operations:

  • add(value?) - Add new item
  • remove(index) - Remove item at index
  • move(from, to) - Move item position
  • swap(i, j) - Swap two items
  • insert(index, value) - Insert at specific position
  • replace(index, value) - Replace item at index

Examples:

// Basic list
<List name="users" initialValue={[{ name: '', email: '' }]}>
  {(fields, { add, remove }) => (
    <>
      {fields.map((field) => (
        <div key={field.key}>
          <Field name={`${field.name}.name`}>
            <input placeholder="Name" />
          </Field>
          <Field name={`${field.name}.email`}>
            <input placeholder="Email" />
          </Field>
          <button onClick={() => remove(field.name)}>Remove</button>
        </div>
      ))}
      <button onClick={() => add()}>Add User</button>
    </>
  )}
</List>

// Advanced list operations
<List name="tasks">
  {(fields, { add, remove, move, swap }) => (
    <>
      {fields.map((field, index) => (
        <div key={field.key}>
          <Field name={`${field.name}.title`}>
            <input />
          </Field>
          <button onClick={() => remove(index)}>Delete</button>
          <button onClick={() => move(index, index - 1)} disabled={index === 0}>
            Move Up
          </button>
          <button onClick={() => move(index, index + 1)} disabled={index === fields.length - 1}>
            Move Down
          </button>
        </div>
      ))}
      <button onClick={() => add({ title: '' })}>Add Task</button>
    </>
  )}
</List>

<ComputedField>

Computed field component that automatically calculates and updates based on other field values.

Props:

| Property | Type | Default | Description | | --------------- | ------------------- | ------------ | ------------------------------------ | | name | string | Required | Computed field name | | deps | string[] | Required | Array of dependent field names | | compute | (get, all) => any | Required | Compute function | | rules | Rule[] | - | Validation rules array | | valuePropName | string | 'value' | Name of the value prop | | preserve | boolean | true | Whether to preserve value on unmount |

Examples:

// Calculate total price
<Form initialValues={{ quantity: 1, unitPrice: 100 }}>
  <Field name="quantity">
    <input type="number" placeholder="Quantity" />
  </Field>

  <Field name="unitPrice">
    <input type="number" placeholder="Unit Price" />
  </Field>

  <ComputedField
    name="totalPrice"
    deps={['quantity', 'unitPrice']}
    compute={(get) => {
      const quantity = get('quantity') || 0;
      const unitPrice = get('unitPrice') || 0;
      return quantity * unitPrice;
    }}
  >
    <input placeholder="Total Price (auto-calculated)" />
  </ComputedField>
</Form>

// Format full name
<Form>
  <Field name="firstName">
    <input placeholder="First Name" />
  </Field>
  <Field name="lastName">
    <input placeholder="Last Name" />
  </Field>

  <ComputedField
    name="fullName"
    deps={['firstName', 'lastName']}
    compute={(get) => {
      const first = get('firstName') || '';
      const last = get('lastName') || '';
      return `${first} ${last}`.trim();
    }}
  >
    <input placeholder="Full Name (auto-generated)" />
  </ComputedField>
</Form>

Hooks

useForm()

Create a form instance for programmatic form control.

const [form] = useForm<FormValues>();

// Form instance methods
form.getFieldValue('username'); // Get field value
form.getFieldsValue(); // Get all field values
form.setFieldValue('username', 'John'); // Set field value
form.setFieldsValue({ username: 'John', email: '[email protected]' });
form.resetFields(); // Reset all fields
form.resetFields(['username']); // Reset specific fields
form.submit(); // Trigger submission
form.validateFields(); // Validate all fields
form.validateFields(['email']); // Validate specific fields
form.getFieldError('email'); // Get field error
form.getFieldsError(); // Get all field errors

useWatch()

Watch form field value changes.

// Watch single field
const username = useWatch('username', { form });

// Watch multiple fields
const { email, phone } = useWatch(['email', 'phone'], { form });

// Watch all fields
const allValues = useWatch(form);

// Use inside form (automatically gets form instance)
function FormContent() {
  const values = useWatch(); // No need to pass form
  return <div>{JSON.stringify(values)}</div>;
}

useFieldState()

Get complete field state (value, errors, validation state, etc.).

const fieldState = useFieldState('email', { form });
// {
//   value: '[email protected]',
//   errors: [],
//   warnings: [],
//   validating: false,
//   validated: true,
//   touched: true
// }

useFieldError()

Get field error information.

const error = useFieldError('email', { form });

useArrayField()

Hook for array field operations, providing complete array operation capabilities.

const { fields, add, remove, move, swap, insert, replace } = useArrayField('users', form);

// Return values
// fields: ListRenderItem[] - Array field items list (with stable keys)
// add: (value?) => void - Add new item
// remove: (index) => void - Remove item at index
// move: (from, to) => void - Move item position
// swap: (i, j) => void - Swap two items
// insert: (index, value) => void - Insert at specific position
// replace: (index, value) => void - Replace item at index

Examples:

function TodoList() {
  const [form] = useForm();
  const { fields, add, remove, move } = useArrayField('todos', form);

  return (
    <Form
      form={form}
      initialValues={{ todos: [] }}
    >
      {fields.map((field, index) => (
        <div key={field.key}>
          <Field name={`${field.name}.title`}>
            <input placeholder="Task Title" />
          </Field>
          <button onClick={() => remove(index)}>Delete</button>
          <button onClick={() => move(index, index - 1)}>Move Up</button>
        </div>
      ))}
      <button onClick={() => add({ title: '' })}>Add Task</button>
    </Form>
  );
}

useFieldEffect()

Create reactive side effects that execute custom logic when specified fields change.

useFieldEffect(
  deps: string[],           // Array of dependent field names
  effect: (get, all) => void, // Effect function
  form?: FormInstance       // Optional form instance
);

Examples:

function UserForm() {
  const [form] = useForm();

  // Log when username changes
  useFieldEffect(
    ['firstName', 'lastName'],
    get => {
      const firstName = get('firstName');
      const lastName = get('lastName');
      console.log(`Name changed to: ${firstName} ${lastName}`);
    },
    form
  );

  // Clear city when country changes
  useFieldEffect(
    ['country'],
    (get, all) => {
      const country = get('country');
      if (country !== all.previousCountry) {
        form.setFieldValue('city', undefined);
      }
    },
    form
  );

  return (
    <Form form={form}>
      <Field name="firstName">
        <input placeholder="First Name" />
      </Field>
      <Field name="lastName">
        <input placeholder="Last Name" />
      </Field>
      <Field name="country">
        <select>{/* Country options */}</select>
      </Field>
      <Field name="city">
        <select>{/* City options */}</select>
      </Field>
    </Form>
  );
}

useSelector()

Select and subscribe to derived form state, only triggering re-renders when the selected value changes.

const value = useSelector<Values, Result>(
  selector: (get, all) => Result, // Selector function
  options?: {
    deps?: string[];              // Dependent fields (empty array = all fields)
    form?: FormInstance;          // Form instance
    eq?: (a, b) => boolean;       // Custom equality comparison function
    mask?: ChangeMask;            // Change type bitmask to subscribe to
    includeChildren?: boolean;    // Whether to include child field changes
  }
);

Examples:

// Calculate shopping cart total
function ShoppingCart() {
  const total = useSelector(
    get => {
      const items = get('items') || [];
      return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
    },
    { deps: ['items'] }
  );

  return (
    <Form>
      <List name="items">
        {(fields, { add, remove }) => (
          <>
            {fields.map(field => (
              <div key={field.key}>
                <Field name={`${field.name}.name`}>
                  <input placeholder="Product Name" />
                </Field>
                <Field name={`${field.name}.price`}>
                  <input
                    type="number"
                    placeholder="Price"
                  />
                </Field>
                <Field name={`${field.name}.quantity`}>
                  <input
                    type="number"
                    placeholder="Quantity"
                  />
                </Field>
                <button onClick={() => remove(field.name)}>Delete</button>
              </div>
            ))}
            <button onClick={() => add({ name: '', price: 0, quantity: 1 })}>Add Product</button>
          </>
        )}
      </List>
      <div>Total: ${total.toFixed(2)}</div>
    </Form>
  );
}

// Use custom equality comparison (avoid re-renders from object reference changes)
function UserProfile() {
  const userInfo = useSelector(
    get => ({
      name: get('name'),
      age: get('age'),
      email: get('email')
    }),
    {
      deps: ['name', 'age', 'email'],
      eq: (a, b) => {
        // Deep equality comparison
        return a.name === b.name && a.age === b.age && a.email === b.email;
      }
    }
  );

  return <div>{JSON.stringify(userInfo)}</div>;
}

useUndoRedo()

Undo/redo functionality.

const { undo, redo, canUndo, canRedo } = useUndoRedo({ form });

🎯 Validation Rules

Built-in Validation Types

<Field
  name="field"
  rules={[
    // Required
    { required: true, message: 'This field is required' },

    // Type validation
    { type: 'email', message: 'Invalid email format' },
    { type: 'url', message: 'Invalid URL format' },
    { type: 'number', message: 'Must be a number' },
    { type: 'integer', message: 'Must be an integer' },
    { type: 'float', message: 'Must be a float' },
    { type: 'boolean', message: 'Must be a boolean' },
    { type: 'date', message: 'Must be a date' },
    { type: 'hex', message: 'Must be a hex color' },
    { type: 'regexp', message: 'Must be a regular expression' },

    // String length
    { minLength: 3, message: 'At least 3 characters' },
    { maxLength: 20, message: 'At most 20 characters' },
    { len: 11, message: 'Must be 11 digits' },

    // Number range
    { min: 0, max: 100, message: 'Must be between 0-100' },

    // Pattern matching
    { pattern: /^[a-zA-Z]+$/, message: 'Letters only' },

    // Enum values
    { type: 'enum', enum: ['admin', 'user', 'guest'], message: 'Invalid role' },

    // Warning only (doesn't block submission)
    { minLength: 8, message: 'Weak password, at least 8 characters recommended', warningOnly: true },

    // Whitespace validation
    { whitespace: true, message: 'Cannot contain only whitespace' },

    // Custom validator
    {
      validator: async (rule, value, allValues) => {
        if (value !== allValues.password) {
          return 'Passwords do not match';
        }
      }
    },

    // Value transformation
    {
      type: 'string',
      transform: value => value?.trim(),
      message: 'Invalid string'
    }
  ]}
>
  <input />
</Field>

Validation Modes

// Serial validation (stops at first error)
form.validateFields({ mode: 'serial' });

// Parallel validation (collect all errors)
form.validateFields({ mode: 'parallelAll' });

// Parallel validation (return first error)
form.validateFields({ mode: 'parallelFirst' });

Schema Validation

import { z } from 'zod';

const schema = z
  .object({
    username: z.string().min(3, 'Username must be at least 3 characters'),
    email: z.string().email('Invalid email format'),
    age: z.number().min(18, 'Must be at least 18 years old'),
    password: z.string().min(8, 'Password must be at least 8 characters'),
    confirmPassword: z.string()
  })
  .refine(data => data.password === data.confirmPassword, {
    message: 'Passwords do not match',
    path: ['confirmPassword']
  });

<Form
  schema={schema}
  onFinish={handleSubmit}
>
  {/* Fields */}
</Form>;

🔧 Advanced Usage

Conditional Fields

function ConditionalFields() {
  const [form] = useForm();
  const userType = useWatch('userType', { form });

  return (
    <Form form={form}>
      <Field name="userType">
        <select>
          <option value="individual">Individual</option>
          <option value="company">Company</option>
        </select>
      </Field>

      {userType === 'company' && (
        <Field name="companyName">
          <input placeholder="Company Name" />
        </Field>
      )}
    </Form>
  );
}

Field Dependencies

function LinkedFields() {
  const [form] = useForm();

  return (
    <Form form={form}>
      <Field
        name="country"
        onChange={value => {
          // Clear city when country changes
          form.setFieldValue('city', undefined);
        }}
      >
        <select>{/* Country options */}</select>
      </Field>

      <Field name="city">
        <select>{/* Dynamically load cities based on country */}</select>
      </Field>
    </Form>
  );
}

Middleware System

// Create logger middleware
const loggerMiddleware = (action, next) => {
  console.log('Action:', action);
  const result = next(action);
  console.log('Result:', result);
  return result;
};

const [form] = useForm();
form.use(loggerMiddleware);

Nested Forms

<Form>
  <Field name="user.profile.firstName">
    <input />
  </Field>
  <Field name="user.profile.lastName">
    <input />
  </Field>
  <Field name="user.contact.email">
    <input />
  </Field>
</Form>

🎨 TypeScript Support

Complete type inference and type safety:

interface FormValues {
  username: string;
  email: string;
  profile: {
    age: number;
    bio: string;
  };
  hobbies: string[];
}

function TypedForm() {
  const [form] = useForm<FormValues>();

  // ✅ Type safe
  const username = form.getFieldValue('username'); // string
  const age = form.getFieldValue('profile.age'); // number

  // ✅ Auto-completion
  form.setFieldsValue({
    username: 'John',
    profile: { age: 25, bio: 'Developer' }
  });

  return (
    <Form<FormValues>
      form={form}
      onFinish={values => {
        // values are fully typed
        console.log(values.username); // ✅
        console.log(values.profile.age); // ✅
      }}
    >
      <Field<FormValues> name="username">
        <input />
      </Field>
      <Field<FormValues> name="profile.age">
        <input type="number" />
      </Field>
    </Form>
  );
}

📖 Best Practices

1. Use External Form Instance for Programmatic Control

function MyForm() {
  const [form] = useForm();

  const handleReset = () => {
    form.resetFields();
  };

  const handleFill = () => {
    form.setFieldsValue({
      username: 'admin',
      email: '[email protected]'
    });
  };

  return (
    <Form form={form}>
      {/* Fields */}
      <button
        type="button"
        onClick={handleReset}
      >
        Reset
      </button>
      <button
        type="button"
        onClick={handleFill}
      >
        Fill
      </button>
    </Form>
  );
}

2. Use useWatch Wisely to Avoid Unnecessary Renders

// ❌ Bad: Entire component re-renders on any field change
function BadExample() {
  const values = useWatch();
  return <div>{values.someField}</div>;
}

// ✅ Good: Only re-renders when someField changes
function GoodExample() {
  const someField = useWatch('someField');
  return <div>{someField}</div>;
}

3. Use Custom Validators for Complex Validation

<Field
  name="password"
  rules={[
    {
      validator: async (rule, value) => {
        // Async validation: check password strength
        const strength = await checkPasswordStrength(value);
        if (strength < 3) {
          return 'Password strength is insufficient';
        }
      }
    }
  ]}
>
  <input type="password" />
</Field>

🤝 Integration with UI Libraries

Skyroc Form seamlessly integrates with any UI library:

// Integration with Ant Design
import { Input, Button } from 'antd';

<Form>
  <Field name="username">
    <Input placeholder="Username" />
  </Field>
  <Button
    type="primary"
    htmlType="submit"
  >
    Submit
  </Button>
</Form>;

// Integration with Material-UI
import { TextField, Button } from '@mui/material';

<Form>
  <Field name="email">
    <TextField label="Email" />
  </Field>
  <Button type="submit">Submit</Button>
</Form>;

📄 License

MIT License

🔗 Links