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

react-simple-formkit

v2.4.7

Published

Support form handling simply. Configuration is simple, fast, and efficient.

Readme

React Simple FormKit

npm version bundle size license npm downloads

A lightweight library for managing forms as uncontrolled. State updates only when actively watched. Simple and quick to configure with outstanding efficiency.

Table of Contents

Quick start

import { useForm } from "react-simple-formkit";

const { control } = useForm();

const handleSubmit = (data) => {
  alert(JSON.stringify(data));
};

return (
  <Form control={control} onSubmit={handleSubmit} onChange={console.log}>
    <button type="submit">Submit</button>
    <input required name="email" placeholder="email" />
    <input required name="password" type="password" placeholder="password" />
    {Array.from({ length: 10 }).map((_, index) => (
      <input name={`input${index + 1}`} placeholder={`input ${index + 1}`} />
    ))}
  </Form>
);

Highlights

  • 🪶 Lightweight — ~20KB, zero dependencies, supports React 17, 18, and 19
  • 📦 Easy data collection — Automatically collects all input values including nested objects, just by using the name attribute
  • 📊 Comprehensive state management — Track isDirty, isTouched, errors at both form level and individual field level
  • 🧠 Unified Observation — One watch() API for values, errors, formState, and fieldStates using dot-notation syntax
  • 👀 Flexible watchingwatch(), useWatch(), and subscribe() all share the same name format, with support for both onChange and onBlur modes
  • 🏷️ Unlimited custom states — Add any custom state to fields and forms (hidden, disabled, verificationStatus, ...)
  • 🔄 Flexible set & reset — Set/get individual field values, reset the entire form or per field, supports callback in setValue
  • 🌳 Nested objects — Group fields into nested structures with dot-notation (address.line1), with event bubble & trickle support

Features

Managing Values

  • Auto collection — All <input>, <select>, <textarea> with a name attribute are automatically collected on change, blur, submit.
  • Flexible get/setactions.getValues(), actions.setValue("field", value), or actions.setValue("field", prev => prev + 1) with callback support
  • Default values — Pass defaultValues on initialization, automatically populates into inputs
  • Resetactions.reset() to default values, or actions.reset(newValues) to update defaults (e.g. after a successful save)

Managing Form State

  • isDirty — Has the form changed from its default values?
  • isError — Does the form have any validation errors?
  • dirtyFields — List of fields that have been modified
  • touchedFields — List of fields that have been interacted with
  • errorFields — List of fields that currently have errors
  • Custom form states — Add any custom state: actions.setFormState("step", 2), actions.setFormState("lastSaved", new Date())

Managing Field States

  • isDirty, isTouched, error for each individual field
  • Custom field states — Unlimited: actions.setFieldState("email", "hidden", true), actions.setFieldState("email", "verificationStatus", "pending")
  • Lazy subscriptionController only subscribes to fieldState when you actually access it in the render function

Managing Errors

  • Custom errorsactions.setError("email", "Email already exists")
  • Clear errorsactions.clearError("email") or actions.clearErrors() to clear all

Watching

Three ways to observe, all sharing the same name format, with support for both onChange and onBlur modes: | Method | Re-render | Best for | |:-------|:----------|:---------| | watch(name, mode) | At the component containing useForm | Simple, quick access | | useWatch({ name, mode }) | Only at the component calling the hook | Performance isolation | | subscribe(name, callback, mode) | No re-render | Side effects | All three use the same unified dot-notation name format to observe values, errors, form state, and field states. See The Power of watch (Unified Observation) for details.

Grouping Fields (Nested Object)

  • Automatically groups fields into nested structures using dot-notation: <input name="address.line1" />
  • watch, setValue, setError, setFieldState all work seamlessly with nested paths
  • Supports event bubble (propagate to parent watchers)

Core concepts

Input Field Modes

1. Uncontrolled:

Best for: High Performance.

  • Use Cases:

    • Standard HTML inputs (e.g. <input>, <textarea>, <select>...)
    • UI library components that act as simple wrappers around native elements and follow standard browser behavior
  • How it works: The browser manages form by default. However, you still maintain full visibility: you can watch the entire form state, field states, and values.

  • onBlur Behavior: Works automatically by default.

Warning: The library automatically captures changes in the Form component through bubbled events from input fields. However, if in some cases it cannot capture changes (e.g., due to a custom input or e.stopPropagation()), please use Controller so the form can work correctly.

2. Controlled:

Best for: Absolute control over input or complex UIs.

  • Use Cases:
    • Advanced UI components with complex INPUTS AND OUTPUTS. For example, a multiple select component expects an array as its input/output, but your form state might need to store the value as a string.

By using Controller, you can transform the value in both directions:

  • On input: split the stored string into an array to pass to the UI component.
  • On change: join the selected array back into a string before updating the form state.

Note: You should explicitly pass the onBlur prop during rendering to use blur tracking features. whenever you want to control the value of a field (e.g. by calling actions.setValue), you must wrap that field with a Controller. Without Controller, the field will not respond to external value changes.

Example:

<Controller
  name="multipleSelect"
  render={({ value = "", onChange, onBlur, name }) => {
    return (
      <Select
        multiple
        name={name}
        onBlur={onBlur}
        value={value.split(",")}
        onChange={(e) => {
          const value = e.target.value;
          // value as array by default but on autofill value as string
          onChange(typeof value === "string" ? value : value.join(","));
        }}
      >
        <MenuItem value="10">Ten</MenuItem>
        <MenuItem value="20">Twenty</MenuItem>
        <MenuItem value="30">Thirty</MenuItem>
      </Select>
    );
  }}
/>

Example fieldState:

  • Only when you get fieldState from render props, Controller will auto subscribe to fieldState changes.
  • If you need to set custom fieldState, you can use actions.setFieldState.
<Controller
  name="multipleSelect"
  render={({ value = "", onChange, onBlur, name, fieldState }) => {
    if (fieldState.hidden) return null;
    return (
      <FormControl error={Boolean(fieldState.error)}>
        <Select
          multiple
          name={name}
          onBlur={onBlur}
          value={value.split(",")}
          error
          onChange={(e) => {
            const value = e.target.value;
            // value as array by default but on autofill value as string
            onChange(typeof value === "string" ? value : value.join(","));
          }}
        >
          <MenuItem value="10">Ten</MenuItem>
          <MenuItem value="20">Twenty</MenuItem>
          <MenuItem value="30">Thirty</MenuItem>
        </Select>
        <FormHelperText>{fieldState.error}</FormHelperText>
      </FormControl>
    );
  }}
/>

Watching for updates

State updates only when observed via watch(), useWatch(), subscribe(), or by tracking changes through the Form component's onChange, onBlur callbacks. You can watch Values, Form States, Field States, and Errors through the same name interface.

Rule*: By default, onBlur works automatically for uncontrolled fields. However, for controlled fields, you must explicitly pass the onBlur prop when rendering the field. Or it can be triggered manually using actions.triggerFieldBlur(). actions.setValue() makes onChange by default, if you call it in onChange callback so it will make infinite loop.

The Power of watch (Unified Observation)

The watch function (and useWatch, subscribe) is the "brain" of your form. Instead of having multiple hooks for different states, you can observe anything in the form using a unified dot-notation syntax:

| Target | Syntax Example | | :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | All Values | watch() | | Specific Value | watch("email") | | Form State | watch("formState") watch("formState.isDirty") watch("formState.isError") watch("formState.dirtyFields") watch("formState.touchedFields") watch("formState.errorFields") | | Field States | watch("fieldStates") watch("fieldStates.email") watch("fieldStates.email.isDirty") watch("fieldStates.email.isTouched") watch("fieldStates.email.isError") watch("fieldStates.email.customState") | | Errors | watch("errors") watch("errors.email") | | Multiple values | watch(["email", "errors.email", "fieldStates.email", "formState.isDirty"]) | | onBlur mode | watch("email", "onBlur") |

💡Why it matters*: This unified approach ensures you only need to learn one API to track the entire lifecycle of your form.

Managing values

Get value

const fieldName = watch("fieldName");
const fieldName2 = watch("fieldName2");
// Or
const { fieldName2, fieldName } = watch(["fieldName", "fieldName2"]);
// Or
const values = watch();

// useWatch
useWatch({ name: "fieldName" });
useWatch({ name: ["fieldName", "fieldName2"] });
useWatch({ compute: (values) => (values.number > 10 ? true : false) });

// actions.getValues()
const { actions } = useForm();
console.log(actions.getValues());

Note*: watch(), useWatch(), and subscribe() share the same name format.

Set value

actions.setValue() can help to control value of a field. But that field must be controlled by Controller.

const { control, actions, watch } = useForm();
const number = watch("number");

return (
  <Form control={control} onChange={console.log}>
    <Controller
      name="number"
      render={({ name, value, onChange }) => (
        <input name={name} value={value} onChange={(e) => onChange(e.target.value)} />
      )}
    />
    <button type="button" onClick={() => actions.setValue("number", Number(number || 0) + 1)}>
      Increase
    </button>
    <button type="submit" disabled={!isDirty}>
      Submit
    </button>
  </Form>
);

Default values and reset

const dummyFields = Array.from({ length: 10 }).reduce(
  (acc, _, index) => ({ ...acc, [`input${index + 1}`]: `input${index + 1}` }),
  {},
);

const { control, watch, actions } = useForm({ defaultValues: dummyFields });

const handleSubmit = async (newValues) => {
  // update to server
  await new Promise((res) => setTimeout(res, 1000));
  // reset with new defaultValues
  actions.reset(newValues);
};

return (
  <Form control={control} onSubmit={handleSubmit} onChange={console.log}>
    {Object.keys(dummyFields).map((name) => (
      <input name={name} placeholder={name} />
    ))}
    <button type="reset" disabled={!isDirty}>
      Reset
    </button>
    <button type="submit" disabled={!isDirty}>
      Submit
    </button>
  </Form>
);

Managing form state

Set form state

actions.setFormState("step", 2);
actions.setFormState("lastSaved", new Date());

Get form state

const isFormDirty = watch("formState.isDirty");
const isFormError = watch("formState.isError");
const dirtyFields = watch("formState.dirtyFields");
const touchedFields = watch("formState.touchedFields");
const errorFields = watch("formState.errorFields");
const customFormState = watch("formState.custom.hello");
// actions.getFormState()
const { actions } = useForm();
console.log(actions.getFormState());
console.log(actions.getFormState("isDirty"));
console.log(actions.getFormState("custom.hello"));
console.log(actions.getFormState(["isDirty", "isError"]));

Note*: watch(), useWatch(), and subscribe() share the same name format.

Managing field states

Set field states

actions.setFieldState("email", "hidden", true);
actions.setFieldState("email", "verificationStatus", "pending");

Get field states

const { fieldName: {...}, fieldName2: {...} } = watch("fieldStates")
const { isDirty, isTouched } = watch("fieldStates.fieldName")
const isFieldDirty = watch("fieldStates.fieldName.isDirty")
const fieldCustomState = watch("fieldStates.fieldName.hidden")
// actions.getFieldStates()
const { actions } = useForm()
console.log(actions.getFieldStates())
console.log(actions.getFieldStates("fieldStates.fieldName.isDirty"))
console.log(actions.getFieldStates("fieldStates.fieldName.hidden"))
console.log(actions.getFieldStates(["fieldStates.fieldName.isDirty", "fieldStates.fieldName.isError"]))

Note*: watch(), useWatch(), and subscribe() share the same name format.

Managing errors

Get field error

const isFormError = watch("formState.isError");
const { fieldName, fieldName2 } = watch("errors");
const fieldError = watch("errors.fieldName");
// actions.getErrors()
const { actions } = useForm();
console.log(actions.getErrors());

Note*: watch(), useWatch(), and subscribe() share the same name format.

Set field error

// actions.setError()
const { control, actions, watch } = useForm();
const { isError, "errors.input1": input1Error } = watch(["isError", "errors.input1"]);

return (
  <Form control={control} onChange={console.log}>
    <input name="input1" />
    {input1Error && <span className="error-message">{input1Error}</span>}
    <button type="button" onClick={() => actions.setError("input1", "This is error message")}>
      Set Error
    </button>
    <button type="submit" disabled={!isDirty}>
      Submit
    </button>
  </Form>
);

Grouping fields (nested object)

  • Use cases: When you need to group multiple fields into an object. By registering these fields with dot notation, you can manage these fieldStates, errors, values as a nested object.

  • Example: Suppose an address group has two fields, line1 and line2. You want to track their individual states (fieldStates, errors, values) separately, but you also need to group them under a single address object for easier management and form submission.

Registering

  • Auto register when using inputs or controllers with dot notation
// uncontrolled
<input name="address.line1" />
<input name="address.line2" />
<input name="address.line3" />
// controlled
<Controller
  name="address.line4"
  render={({ value = "", onChange }) => {
    return <input value={value} onChange={onChange} />;
  }}
/>
  • Register manually
// 'groups' option
const { control, actions } = useForm({ groups: ["address"] });
// actions.addGroups
actions.addGroups(["address"]);

Rule*: If a group isn't registered, the field will be treated as a regular field with an object type value.

Watching

const addressValue = watch("address");
const addressLine1 = watch("address.line1");
const addressLine1Error = watch("errors.address.line1");
const addressLine1FieldState = watch("fieldStates.address.line1");

Updating

const { actions } = useForm();
actions.setValue("address.line1", "hello");
actions.setError("address.line1", "error message");
actions.setFieldState("address.line1", "hidden", true);
actions.setFieldState("address.line1", "disabled", true);
actions.setFieldState("address.line1", "custom", { hello: "world" });

Rule*: If a group is registered, all the fieldState, error, and value will be stored in the bottom level fields (leaf nodes). You cannot set fieldState, error, and value for a group (e.g. "address").

Utilities

restoreFromDotNotation(object)

Convert dot notation objects to nested objects

import { restoreFromDotNotation } from "react-simple-formkit";

const dotNotationObject = watch(["errors.email", "errors.password"]);
const nestedObject = restoreFromDotNotation(dotNotationObject);
// Output:
// {
//   "errors": {
//     "email": "Email is required",
//     "password": "Password is required",
//   }
// }

APIs

useForm

Generic props:

  • defaultValues: Object Example
  • shouldUnRegister: Boolean Default is false,
  • groups: Array to register field groups, Example

Return:

  • control: contains methods and utilities to control the form.
  • watch(name, mode): (name: String | Array | undefined, mode: "onChange" | "onBlur") => Object Example
  • actions is an object that contains utilities
    • actions.reset(): (newDefaultValues?: Object, options?: { clearCustomFormStates?: Boolean, clearCustomFieldStates?: Boolean }) => void Example
    • actions.getValues(): (name?: String | Array) => Object Example
    • actions.getErrors(): (name?: String | Array) => Object Example
    • actions.getFormState(): (name?: String | Array) => Object Example
    • actions.getFieldStates(): (name?: String | Array) => Object Example
    • actions.setValue(): (name: String, value: Any, options?: { shouldDirty?: Boolean, shouldTouched?: Boolean }) => void Example
    • actions.setError(): (name: String, error: Any) => void Example
    • actions.setFieldState(): (name: String, property: String, value: Any) => void Example
    • actions.setFormState(): (name: String, value: Any) => void
    • actions.triggerFieldBlur (name: String, value?: Any) => void`. Used to trigger the blur event for a field manually.
    • actions.resetFieldState(): (name: String) => void
    • actions.resetField(): (name: String) => void
    • actions.clearError(): (name: String | Array) => void
    • actions.clearErrors(): () => void
    • actions.subscribeChange(): (callback) => unsubscribe: Function()
    • actions.subscribeBlur(): (callback) => unsubscribe: Function()
    • actions.getNumberFields(): () => Array
    • actions.getDefaultValues(): () => Object
    • actions.trigger(name, options): (name: String | Array, options?: {bubble?: Boolean, trickle?: Boolean}) => void trigger watchers (e.g. watch, useWatch, subscribe) re-update values if needed.
      • bubble: Boolean. If true, it will trigger all parent events (e.g. trigger "address.line1" will trigger watch("address"), watch()).
      • trickle: Boolean. If true, it will trigger all child events (e.g. trigger "address" will trigger watch("address.line1"), watch("address.line2"), etc.).
    • actions.addGroups(): (name: String | Array) => void Example

Form

Generic props:

  • control: received from useForm()
  • onSubmit: (currentValues) => {} called when the form is submitted via a button with type='submit'
  • onChange: (name, value, currentValues) => {} called when any field value changes
  • onBlur: (name, value, currentValues) => {} called when any field blurred

Controller

Generic props:

  • name: String
  • defaultValue: Any
  • shouldUnRegister: Boolean Default is false
  • control: received from useForm(). Pass it if use Controller outside <Form>
  • render({name, value, onChange, onBlur, fieldState})

useController

Generic props:

  • name: String
  • defaultValue: Any
  • shouldUnRegister: Boolean Default is false
  • control: received from useForm(). Pass it if use Controller outside <Form>

Return: render arguments in <Controller>

useWatch

Example

Generic props:

  • name: String | Array | undefined. If undefined, it will return all input values
  • mode: "onChange" | "onBlur". Default is onChange
  • compute: Function that will calculate from form values and return a value. It will make re-render when the result changes
  • control: received from useForm(). Pass it if use Controller outside <Form>

Return:

  • if compute is passed: the computed value
  • if name is string: value of the field
  • if name is array: object of values of the fields
  • if name is undefined: object of all input values

useFormContext

Return:

  • watch(name, mode)
  • actions: same with useForm().actions

watch

Arguments: watch(name, mode)

  • name: String | Array | undefined. If undefined, it will return all input values
  • mode: "onChange" | "onBlur". Default is onChange

Return:

  • same with useWatch()

subscribe

Arguments: subscribe(name, callback, mode)

  • name: String | Array | undefined. If undefined, it will return all input values
  • callback(): Function receive value based on name parameter. Only be called when the value changes
  • mode: "onChange" | "onBlur". Default is onChange

Return:

  • unregister(): Function that will unregister the callback

Examples

  • https://codesandbox.io/p/sandbox/react-simple-formkit-examples-rhmhjj

Contact

For any ideas or issues, please contact me at [email protected]