react-final-form

A thin React wrapper for Final Form, a subscriptions-based form state manager that re-renders only the fields whose state actually changed.

Library
npm
v7.0.1
7,438stars
MIT License

Repository Health

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

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
80/100Excellent
Architecture78
Code Quality90
Innovation68
Learning Curve85

React Final Form is the official React binding for Final Form, a framework-agnostic form state library built on the observer pattern. Instead of re-rendering an entire form on every keystroke, each Field and hook subscribes only to the exact pieces of form or field state it needs, so unrelated inputs never re-render when one field changes.

The library ships as a thin layer over Final Form’s FormApi: Form creates and holds the form instance, Field/useField register individual inputs directly against it, and FormSpy/useFormState expose read-only access to form state for components that need to react to it (a submit button, a debug panel, a summary) without being a field themselves. Validation, formatting/parsing, and array fields are all opt-in through configuration rather than required setup, and withTypes<FormValues>() gives TypeScript consumers a fully typed Form/FormSpy pair for their own value shape in one line.

With zero runtime dependencies beyond a tiny Babel helper and peer dependencies on React and Final Form, it stays under a 4KB gzip budget enforced by CI, making it a common choice for teams that want fine-grained control over form re-renders without adopting a heavier state-management library.

What You Get

  • Form component that creates and manages a Final Form instance and exposes handleSubmit, form state, and the underlying FormApi via render props or children
  • Field component and useField hook for registering individual inputs with their own subscription, format/parse, and validation config independent of the rest of the form
  • FormSpy component and useFormState hook for subscribing to form-level state (validity, submitting, dirty, values) from anywhere in the tree without being a field
  • useForm hook for accessing the Final Form API imperatively (e.g. to call form.change, form.reset, or form.batch from custom logic)
  • withTypes<FormValues>() helper that returns pre-typed Form/FormSpy components bound to a consumer’s own form value shape
  • Opt-in per-field subscription objects so a field only re-renders on the specific meta flags (touched, invalid, dirty, etc.) it actually reads

Common Use Cases

  • Multi-step or long forms (checkout, onboarding, settings pages) where re-rendering the whole form on every keystroke would be noticeably slow
  • Forms with cross-field or async validation, using Final Form’s validate/validateFields hooks surfaced through Field and Form config
  • Field arrays and dynamically added/removed inputs (line items, tags, repeatable groups) built on Final Form’s array mutators
  • Auto-save or debounced-save forms that watch form state via FormSpy/useFormState and submit in the background without a manual submit button
  • Design-system integrations (Chakra, Downshift, custom inputs) where Field’s component/render/children API adapts to any input component’s prop shape

Under The Hood

Architecture React Final Form wraps final-form (the framework-agnostic form state engine) in a thin React layer. src/ReactFinalForm.tsx creates and holds the FormApi instance via useConstant, pauses validation until all fields have registered on first render and resumes it in a useEffect, subscribes to form state changes with a computed subscription object, and provides the FormApi to descendants through React Context (src/context.ts). src/useField.ts and src/Field.tsx register individual fields directly against the FormApi via form.registerField, each keeping its own subscription-driven local state so unrelated field changes don’t cause sibling re-renders. src/FormSpy.tsx offers a lower-level subscription to form state for components that aren’t fields. src/getters.ts implements a lazy-getter pattern (addLazyFormState/addLazyFieldMetaState) so touching a render-prop property computes it on read rather than materializing the whole state object eagerly. All three render styles (component/render/children) funnel through a shared renderComponent.ts dispatcher. Because useField and FormSpy both depend directly on the same FormApi instance’s registerField/subscribe methods, a change to how ReactFinalForm constructs or wraps that instance would ripple through both.

Tech Stack Authored in TypeScript (~5.8) targeting React 16.8 through 19 and Final Form ^5.0.0, both as peer dependencies, with no runtime dependencies beyond @babel/runtime. Built with Rollup into CJS/ESM/UMD bundles under dist/, transpiled via Babel’s env/react/typescript presets, and size-budgeted at 4KB gzip per bundle via size-limit. Tests run under Jest with jest-environment-jsdom and @testing-library/react. Linting uses ESLint 9’s flat config with typescript-eslint, formatting is Prettier-enforced through husky and lint-staged pre-commit hooks, and task orchestration goes through nps/package-scripts.js. GitHub Actions CI runs lint, a Prettier check, and unit tests with coverage uploaded to Codecov on every push. As a published npm package there’s no deployment target beyond the registry.

Code Quality Each core module has a co-located *.test.js file (Field, FormSpy, ReactFinalForm, useField, useForm, useFormState, shallowEqual, getValue, useConstantCallback, context, renderComponent), plus targeted regression tests named after the GitHub issues that motivated them (useField.issue-984.test.js, useField.dynamic-name-869.test.js), showing an issue-driven testing habit rather than only feature-driven tests. The whole library is written in TypeScript, and a separate typescript/ directory holds dtslint-validated type-only fixtures that check the published .d.ts output stays correct. Error handling is largely delegated to Final Form itself; React Final Form’s own code favors defensive checks (isSyntheticEvent, allowNull handling) over throwing. Naming is consistent and idiomatic (hook names prefixed use, PascalCase components), and ESLint/Prettier/husky enforce style pre-commit, though there’s no enforced coverage threshold beyond the Codecov upload.

API Design The public surface is deliberately small: three components (Form, Field, FormSpy) and three hooks (useField, useForm, useFormState) cover essentially every use case, and withTypes<FormValues>() gives consumers fully typed Form/FormSpy components for their own value shape without repeating generics at every call site. Getting started needs no boilerplate beyond a <Form onSubmit={...} render={...}> wrapping one <Field name="..." component="input" /> per input; validation, formatting, and subscriptions are opt-in props rather than mandatory configuration. The subscription concept — choose exactly which state changes should trigger a re-render — is applied consistently across every entry point, so learning it once transfers to Field, FormSpy, useField, and useFormState alike.

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