Why I Built a Form Library Instead of Reaching for One
I'm writing this partly as a build log and partly as an argument. The build log part is straightforward: I made a form state library called ForMesh, and I want to explain what it does and why it's put together the way it is. The argument part is the more interesting bit to me — I want to make the case that "just use Formik" or "just use React Hook Form" isn't always the right answer, and that sometimes the more disciplined move is to sit with a recurring problem long enough to actually understand its shape before reaching for a dependency. If you build React apps for a living, especially anything in the ERP, CRM, or internal-tools space, I think this will feel familiar. If you're earlier in your career and wondering when it's actually worth building something instead of installing it, I hope this is a useful, honest data point either way.
Where this problem actually showed up
I lead frontend development on an ERP platform — eight modules covering HR, inventory, procurement, manufacturing, budgeting, and a few more, all built for garment and apparel manufacturers. If you've never worked on software like this, the thing that surprises people is how much of it is just forms. Not simple forms — an employee record alone might have Personal Info, Job Info, Family Info, Bank Info, and Documents, each built as its own component, each with its own fields, all of which need to collapse into a single object by the time someone hits Save.
Early on, we solved this the way most teams do: a local useState per section, a hand-written hook to debounce and sync that section's state up into a parent object, and a change handler that had to sniff out whether it was dealing with a native input event, a checkbox, a Date from a picker, or a { value } option object from a custom dropdown. Validation was mostly a ternary — !value ? "border-red-600" : "border-gray-300" — which told you a field was empty, but never told you why it mattered, and never produced an actual error message a user could read.
None of this was broken, exactly. It worked, module after module, form after form. But I kept noticing the same roughly 150 lines of plumbing getting rewritten, slightly differently, by whoever touched that file next. Different naming, different edge cases handled, different bugs. That kind of repetition is exactly where inconsistency creeps in — one form validates required fields, another doesn't; one handles file uploads eagerly, another waits until submit; one has real dirty-state tracking, another doesn't bother.
At some point I stopped patching individual forms and started asking a different question: what is the actual, general shape of this problem, independent of this specific ERP? That question is what became ForMesh.
Why I didn't just reach for an existing library
I want to be fair to the existing options, because they're genuinely good at what they do. Formik and React Hook Form both handle the common case — one form, one set of fields, submit it — very well. But the case I kept running into wasn't that. It was several independent sections, built by different people, sometimes on different days, that needed to merge into one object without any of them knowing about each other's internals. Retrofitting that composition pattern on top of a single-form-shaped library felt like fighting the tool as much as using it.
There was also a performance angle I couldn't ignore. Some of these forms have dozens of fields across their sections. A naive shared-state approach — Context, most obviously — re-renders every consumer on every keystroke, because Context has no built-in way to know that a component only cares about one specific field. I wanted something where that kind of fine-grained update was the default behavior, not something you had to carefully engineer with memoization and custom equality checks on a case-by-case basis.
So I built it. Slowly, in phases, testing each layer before adding the next one — because the fastest way to end up with an unmaintainable form library is to build validation, file handling, and array support all at once on top of a store you haven't actually pressure-tested yet.
What ForMesh actually does
At its core, ForMesh is a small, framework-agnostic form state library, published as react-formesh on npm. Here's what it gives you, concretely:
Sections are a first-class primitive, not a convention you maintain by hand. Each section of a form is built as if it owns its own store. It doesn't — every write actually lands on a shared parent object — but the component never has to think about that part:
tsx const store = createFormStore({ employeeInfo: { firstName: "", lastName: "" }, jobInfo: { department: "", designation: "" }, bankInfo: { accountNumber: "", bankName: "" }, });
function EmployeeInfoSection() { const form = useForm(store, { section: "employeeInfo" }); return <input {...form.registerField("firstName")} />; }
The parent always ends up with the complete, merged object. This is the exact pattern I used to hand-wire per component; now it's just how the library behaves.
Fine-grained re-renders, without any selector API to get wrong. The store is immutable and uses structural sharing — writing to employeeInfo.firstName only clones the objects along that specific path, leaving every sibling branch, like jobInfo, referentially untouched. That one property is enough for React's own useSyncExternalStore to filter out irrelevant re-renders automatically: a component subscribed to a single field just returns that field's value as its snapshot, and React skips rendering whenever that reference hasn't actually changed. No memoized selectors, no dependency arrays, no React.memo scattered around defensively.
Debounced sync and derived fields, unified into one idea. Long forms need writes to batch, so twenty fields don't each trigger a separate parent sync on every keystroke. A single createDebouncedSync wrapper handles this for any target — a whole store, one section, or eventually a row inside a repeatable array — buffering writes and committing them together after a short quiet period, while reads stay live the entire time so the UI never visibly lags. Derived and cascading fields get their own primitive, watch, for cases like clearing a "City" field when "Country" changes:
tsx useWatch(store, "jobInfo.department", (department) => { store.setValue("jobInfo.designation", ""); });
Validation as plain functions, not a schema language to learn. A validator is just (value, context) => message | undefined. The built-ins — required, email, minLength, cross-field rules like confirming a password match — compose exactly the same way a one-off domain rule does, like checking that a SKU isn't already in use. Nothing to import if you don't need it, since it lives in its own /validators entry point, separate from the core.
Small, on purpose. I didn't want a general-purpose utility library pulled in just to do dot-path get/set and object diffing. Those are hand-rolled, in well under 200 lines total. The result is a core store around 3.4 KB gzipped, and React bindings adding roughly 3.9 KB on top — and because every capability lives behind its own entry point, you only pay for what you actually import.
TypeScript throughout, with a package structured for tree-shaking from day one — ., /react, /validators, /file, and /array are separate, independently importable pieces of one package, not one monolithic bundle.
Who this is actually for
If you're building a single contact form or a login screen, you don't need this — reach for whatever you already know, or nothing at all. ForMesh is aimed at a more specific situation: forms that are made of multiple independent sections, forms that are long enough that re-render performance actually matters, and codebases where the same form patterns get rebuilt often enough that consistency starts to matter more than raw flexibility. ERP systems, CRMs, internal admin tools, anything with a "create employee" or "create invoice" screen that spans several tabs — that's the territory this was built for, even though nothing in the library itself knows or cares that it started life inside an ERP.
What's still ahead
File uploads and repeatable field arrays — the two pieces most real multi-section forms eventually need — are actively in progress, not finished yet. I'd rather be upfront about that than dress up a roadmap as a changelog. Everything documented above is built, tested, and something I'd stand behind today; the rest is coming, and I'll write about it here when it lands.
Why I'm writing this down
Partly for other engineers who've hit the same wall and are wondering whether it's worth building something instead of installing it — I hope this is a useful, honest data point either way. And partly for myself: writing out why a design decision was made is a good forcing function for finding out whether it was actually a good decision, or just the first one that occurred to me at 11pm while staring at another copy-pasted handleFieldChange function.
If you're building forms that have outgrown useState, I'd genuinely like to hear what breaks when you try it.
#react_form_library #formik_alternative #useSyncExternalStore
