final-form

Framework-agnostic, subscription-based form state management with zero dependencies.

Library
npm
v5.0.1
3,045stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
57/100Fair
Development Activity16
Maintenance44
Community68
Maturity60
Momentum40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
79/100Good
Architecture85
Code Quality82
Innovation68
Learning Curve80

Final Form is a framework-agnostic form state management library built around a subscription model: instead of re-rendering an entire form on every keystroke, components subscribe only to the specific pieces of form or field state they care about (value, error, touched, dirty, etc.), so only the parts of the UI affected by a change actually update. The core createForm API has zero runtime dependencies and works with any rendering layer, which is why the ecosystem has grown official bindings for React (react-final-form), Vue, Angular, and Svelte on top of the same engine.

At its center is a single form instance created with createForm(config), which registers fields, tracks internal mutable state, runs synchronous or async validation, and notifies field- and form-level subscribers through a fine-grained diffing pass (filterFieldState/filterFormState) that only emits new state objects when a subscribed key actually changed. Field arrays, nested/dot-path values (via its own getIn/setIn implementation), record-level and field-level validation, and custom mutators (for operations like array push/pop/swap) are all handled inside this same engine rather than bolted on by the framework bindings.

The library has been stable and widely adopted since 2017, is maintained by its original author Erik Rasmussen (also known for Redux Form), and is used as a dependency by companies including Salesforce, Cisco, and Atlassian. It ships as TypeScript source with full type definitions and is built for tree-shaking-friendly bundles (ESM, CJS, and UMD outputs) at roughly 5.1kB gzipped.

What You Get

  • A framework-agnostic createForm engine with zero runtime dependencies that any UI layer can bind to
  • Fine-grained subscriptions at both the form level and the individual field level, so only affected consumers re-render
  • Built-in support for synchronous and asynchronous field-level and record-level (whole-form) validation
  • Custom mutators for programmatic state changes (array push/pop/swap, custom field updates) beyond simple change/blur/focus
  • Native dot-path and array-path value access via its own getIn/setIn implementation, so deeply nested and array-shaped form values work without extra tooling
  • Full TypeScript typings generated from the source, plus ESM/CJS/UMD build outputs for any bundler

Common Use Cases

  • Multi-step or wizard-style forms where only the active step’s fields should re-render on input
  • Large forms (settings pages, checkout flows, admin editors) with dozens of fields where naive re-rendering causes visible input lag
  • Dynamic field arrays — repeatable line items, tags, or nested groups — using the built-in array mutators
  • Cross-field and async validation (e.g. checking username availability or validating a field against a sibling field’s value)
  • Building a custom form UI layer for a framework without an official binding, using the framework-agnostic core directly

Under The Hood

Architecture The engine centers on a single closure returned by createForm() (src/FinalForm.ts, ~1,440 lines) that holds an InternalState object combining mutable form/field state with a registry of form- and field-level subscriber sets. State changes flow through a single notify-style pipeline: any mutation (field register/change/blur, validation resolving, a mutator running) updates the internal mutable state, recomputes derived state (hasAnyError, dirty/pristine, submit status), and then calls filterFormState/filterFieldState to diff the new state against each subscriber’s last-seen state before invoking only the subscribers whose subscribed keys actually changed. Field path resolution runs through a self-contained structure/getIn and structure/setIn pair rather than a third-party path library, keeping the core dependency-free. This design cleanly separates the state engine from rendering: framework bindings (react-final-form, etc.) only need to call form.subscribe/registerField and never touch the diffing logic directly, so the core’s correctness is independent of any specific UI framework’s rendering model.

Tech Stack Written entirely in TypeScript (98.6% of the codebase) targeting ES2018, compiled with the TypeScript compiler for type declarations and bundled with Rollup (rollup.config.mjs) into ESM, CJS, and UMD outputs plus minified variants, gated by size-limit budgets (7kB UMD, ~6.5-6.6kB ESM/CJS gzipped) enforced in CI. Babel handles the actual JS transpilation via @babel/preset-typescript and @babel/preset-env, with @babel/runtime as the package’s only runtime dependency. The dev toolchain uses nps (npm-package-scripts) to orchestrate lint/test/build tasks, husky + lint-staged for pre-commit formatting, and yarn as the lockfile-managed package manager.

Code Quality The test suite is extensive and colocated with source (28+ *.test.ts files covering FinalForm.ts itself across dedicated files for async validation, batching, mutators, field registration, reset/modified state, and subscribing behavior, plus focused unit tests for getIn/setIn, shallowEqual, memoize, and isPromise), run with Jest and coverage reporting, and gated in CI (.github/workflows/ci.yml) across separate lint, prettier, and test jobs. ESLint (with TypeScript, React, and a11y plugins) and Prettier are both enforced via lint-staged pre-commit hooks and CI jobs, so style and lint issues can’t land on the default branch. TypeScript’s strict mode is explicitly disabled in tsconfig.json, which is a real gap for a library this size, but the extensive behavioral test coverage substantially offsets weaker compile-time guarantees.

What Makes It Unique Most form libraries either couple state management directly to a specific framework’s reactivity model or accept the cost of re-rendering the whole form tree on every keystroke. Final Form’s differentiator is doing neither: the subscription/diffing engine is entirely framework-agnostic and sits below the rendering layer, which is what let the same core power official bindings for React, Vue, Angular, and Svelte without duplicating validation or mutation logic in each one. Its built-in mutator system (rather than requiring consumers to hand-write array manipulation against a plain object tree) and its dependency-free getIn/setIn path implementation are both deliberate choices to keep the core minimal and portable rather than reaching for lodash.get/immer-style tooling.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search