Final Form
Final Form is a high-performance, framework-agnostic library for managing form state in JavaScript applications. It provides a subscription-based model, meaning components only re-render when the specific pieces of state they subscribe to change, leading to optimized performance. The current stable version is 5.0.0, which notably converted the entire codebase from Flow to TypeScript, enhancing developer experience for TypeScript users. While core API stability is maintained across minor versions, major versions are bumped cautiously, as seen with v5.0.0, to reflect significant internal changes. Key differentiators include its zero-dependency footprint, small bundle size (around 5.1kB gzipped), and its explicit opt-in subscription model, giving developers fine-grained control over re-renders, making it suitable for complex form interactions across various UI frameworks.
Common errors
-
TS2345: Argument of type 'Partial<FormState<T>>' is not assignable to parameter of type 'FormSubscription'.
cause Attempting to pass a partial FormState object as a subscription parameter instead of a FormSubscription object.fixEnsure that the second argument to `form.subscribe` is a `FormSubscription` object, where keys are state properties and values are booleans indicating subscription interest, e.g., `{ values: true, errors: true }`. -
TypeError: createForm is not a function
cause Incorrect CommonJS `require` syntax or mixing CommonJS with ESM in a module that expects ESM imports.fixUse ES module import syntax: `import { createForm } from 'final-form';`. If in a CommonJS-only environment (e.g., older Node.js scripts), ensure your bundler or environment correctly transpiles ESM or use dynamic import if supported. -
Property 'someField' does not exist on type 'FormState<MyFormData>'. Did you mean 'values'?
cause Directly accessing field values on the `FormState` object rather than through the `formState.values` property.fixAccess form values via `formState.values.someField` and errors via `formState.errors.someField`. `FormState` itself contains metadata about the form, not the field values directly.
Warnings
- breaking Version 5.0.0 converted the entire library from Flow to TypeScript. While no *intentional* API breaking changes were introduced, developers relying on Flow types or intricate type inference might experience minor issues or require adjustments to their TypeScript configurations. Always test thoroughly when upgrading.
- gotcha Final Form's subscription model is powerful but requires careful selection of subscribed state. Subscribing to too much state can negate performance benefits, while subscribing to too little might cause components not to re-render when expected.
- gotcha Asynchronous validation in Final Form can sometimes lead to unexpected behavior, such as errors not clearing or validations running at incorrect times, especially if not handled correctly with debouncing or when field values change rapidly.
- gotcha The `allValues` argument for `FieldValidator` was made optional in v4.20.7, but this change was reverted in v4.20.9. This inconsistency might cause type errors or unexpected runtime behavior if your validation functions rely on its optionality or presence across these specific versions.
Install
-
npm install final-form -
yarn add final-form -
pnpm add final-form
Imports
- createForm
const createForm = require('final-form')import { createForm } from 'final-form' - FormApi
import { FormApi } from 'final-form'import type { FormApi } from 'final-form' - FieldState
import { FieldState } from 'final-form'import type { FieldState } from 'final-form'
Quickstart
import { createForm } from 'final-form';
interface MyFormData {
firstName: string;
lastName: string;
age: number;
}
const form = createForm<MyFormData>({
initialValues: { firstName: 'John', lastName: 'Doe', age: 30 },
onSubmit: async (values) => {
await new Promise(resolve => setTimeout(resolve, 500));
console.log('Form submitted with values:', values);
},
validate: (values) => {
const errors: Partial<MyFormData> = {};
if (!values.firstName) {
errors.firstName = 'Required';
}
if (!values.lastName) {
errors.lastName = 'Required';
}
if (values.age < 18) {
errors.age = 'Must be 18 or older';
}
return errors;
},
});
const unsubscribe = form.subscribe((formState) => {
console.log('Form State Changed:', {
values: formState.values,
errors: formState.errors,
valid: formState.valid,
dirty: formState.dirty,
submitting: formState.submitting,
});
}, { values: true, errors: true, valid: true, dirty: true, submitting: true });
// Simulate user input
form.change('firstName', 'Jane');
form.change('age', 17);
// Simulate form submission
form.submit();
// Cleanup (in a real app, this would be on unmount)
// unsubscribe();