final-form-arrays
Array mutators for Final Form that add push, pop, insert, remove, move, swap, and more to manage dynamic list fields.
Repository Health
Technical Analysis
final-form-arrays is a small mutator plugin for Final Form that adds first-class support for array fields — the repeatable rows you see in forms like “add another phone number” or line-item invoices. Instead of hand-rolling index-shifting logic every time a list field changes, it registers a set of mutators (push, pop, insert, remove, removeBatch, move, swap, shift, unshift, update, concat) directly onto Final Form’s mutator API, so array manipulation becomes a single function call from anywhere in the form.
Under the hood, each mutator does more than splice the underlying array value: it also walks Final Form’s internal field-state map and re-keys every affected name[index] field so validation state, touched/visited flags, and subscriptions stay attached to the right row after an insert, removal, or reorder. This is the part most hand-written array-field code gets wrong — moving row 2 to row 0 without this library typically drops or misattributes per-row error state.
The library is deliberately narrow in scope: it has no React bindings of its own (that’s react-final-form-arrays) and ships as a peer-dependency plugin, so it works with any Final Form integration (React, vanilla JS, or other framework bindings) equally.
What You Get
- Ten array mutators —
push,pop,shift,unshift,insert,remove,removeBatch,move,swap,update, andconcat, covering every common array operation a form needs. - Field-state re-indexing — after any mutation, the internal
name[index]field map is rewritten so validation errors, touched/visited flags, and subscriptions stay attached to the correct row instead of leaking to the wrong index. - Framework-agnostic core — ships as a plain Final Form mutator plugin with a peer dependency on
final-form, so it works with any binding (react-final-form, vanilla JS, or custom integrations). - Typed API surface — full TypeScript typings (
DefaultType,Mutators) describing every mutator’s signature, so consumers get autocomplete and type checking without writing their own ambient types. - Regression-hardened edge cases — dedicated test suites for specific historical bugs (mismatched field shapes on move, batch-remove ordering, empty-array removal, submit-error splicing) rather than only happy-path coverage.
Common Use Cases
- Dynamic contact/phone lists — a form where a user can add or remove multiple phone numbers or email addresses, each with its own per-row validation.
- Line-item invoices or order forms — repeatable rows of product/quantity/price where rows can be inserted, reordered, or bulk-removed.
- Tag or chip inputs — pushing and removing individual tag values from an array field while preserving validation state on the remaining tags.
- Drag-to-reorder form sections — using
moveandswapto reorder array items (e.g. prioritized task lists) without losing per-item field state. - Wizard-style multi-entry steps — building up a list of entries (e.g. multiple beneficiaries in an insurance form) across several submit-and-add-another interactions.
Under The Hood
Architecture
The library is organized as one small, single-purpose module per mutator (insert.ts, remove.ts, move.ts, swap.ts, push.ts, pop.ts, shift.ts, unshift.ts, update.ts, removeBatch.ts, concat.ts), each implementing Final Form’s Mutator<any> interface and composed into a single default export plus named exports in src/index.ts. Every mutator follows the same two-phase pattern: first call Final Form’s changeValue tool to splice/update the underlying array value, then walk state.fields with a regex matching name[index] keys to shift, drop, or remap per-field state so validation/touched/visited tracking survives the reindex — this second phase is factored into a shared copyField helper that decides whether to keep a field’s existing change/blur/focus handlers or inherit them from the field it’s replacing (added specifically to fix a differently-shaped-fields bug in move). There is no central store or class hierarchy; the “core abstraction” is simply the field-state re-indexing convention, and every mutator would break identically if Final Form changed how state.fields keys are structured.
Tech Stack
Written in TypeScript and built with Rollup (rollup.config.mjs) into ES, CommonJS, and minified UMD bundles under dist/, with type declarations emitted separately via tsc --declaration --emitDeclarationOnly. final-form is a peer dependency (not a hard dependency), meaning consumers bring their own compatible version. Tooling includes ESLint 9 (flat config) with TypeScript, import, and a11y plugins, Prettier for formatting, Husky + lint-staged for pre-commit enforcement, and size-limit to keep the minified UMD bundle under 2kB. Tests run on Jest with ts-jest.
Code Quality
Testing is extensive relative to the library’s size — nearly every mutator has its own *.test.ts file, several of them (move.different-shapes-51, remove.move-bug-49, remove.submiterrors-47, remove.empty-array-95, removeBatch.lexicographic-bug, unshift.regression-44) are named after specific GitHub issue numbers, indicating a practice of adding a regression test alongside every bug fix. Error handling is minimal by design (the mutators are pure state-transformation functions with no I/O), and type safety leans on TypeScript throughout, though internal field manipulation uses index signatures ({ [key: string]: any }) rather than fully-typed field shapes. ESLint and Prettier are enforced via lint-staged pre-commit hooks; there is no separate CI config visible in the shallow clone, but package-scripts.js/nps orchestrates test and validate steps.
What Makes It Unique
The library’s value isn’t the array operations themselves (trivial with Array.prototype) — it’s that Final Form tracks field-level metadata (touched, visited, validators, subscriptions) keyed by string field name, and naively splicing an array value leaves that metadata pointing at stale indices. final-form-arrays’ contribution is a small but carefully tested re-indexing algorithm (visible in the regex-based key rewriting shared across mutators and the edge-case regression tests) that keeps per-row form state correctly attached to its row through inserts, removals, and reorders — a problem every hand-rolled dynamic-array form implementation has to solve and often gets subtly wrong.