AI News Hub Logo

AI News Hub

Forms Aren’t UI: The Architecture Mistake Most React Apps Make

DEV Community
yanggmtl

From UI Components to Runtime Architecture: The shift that fixed how I build forms at scale. I used to think forms were just UI. You open a React component, add a few inputs, wire validation, handle submit, and ship it. The form works. Nothing feels wrong. That’s how I built forms for a long time. The first few forms were fine. But over time, patterns started repeating—and so did the friction. Then came the questions that always change the scope: can we update this form without a redeploy? can non-frontend teams configure parts of it? can the backend return the structure? can we reuse this flow across apps? In React, this turned into nested conditionals, duplicated components, and logic scattered across the UI. That was the moment it stopped feeling like “just a form”. I thought I had a React problem. extracted reusable components added hooks built better abstractions reorganized the component tree But none of that fixed the core issue. The problem was that the form definition itself was trapped inside the UI. Once I looked closely, it became obvious. tied to a specific framework coupled to deployment cycles hard for backend systems to generate difficult to version as data awkward to reuse across applications That’s when my mental model changed. Forms are not just UI. The Exciting Phase: Schema-Driven Forms Moving forms into a schema felt like a breakthrough. Now the structure could come from: an API a database a CMS a configuration layer React was no longer the author—it became the renderer. This solved real problems: forms became portable backend-driven workflows became possible less JSX needed to be written It felt like the right direction. The Second Problem: Dynamic ≠ Clean As requirements grew, the schema started absorbing more responsibility: validation logic conditional visibility edge cases submission behavior Slowly, the schema stopped being data. mini programming language. And I ended up in a familiar place again—just in a different form. Instead of messy components, I had bloated schemas. Dynamic forms didn’t solve the problem. The Real Question At that point, the question changed. Not: How do I make forms dynamic? But: How do I keep the definition clean while still supporting real behavior? The Key Insight: Separate the Concerns Underneath everything, a form has three distinct concerns: What the form is (structure) How the form behaves (logic) How the form is rendered (UI) Most systems mix these together. The most important rule I arrived at was this: Behavior logic must be separated from both the definition and the renderer. Without that separation, you usually end up choosing between two problematic options: Logic inside the schema Logic inside components Neither scales well. The solution I landed on was simple but powerful: Example: { "name": "signup", "submitterRef": "createAccount", "properties": [ { "name": "vatId", "type": "text", "visibilityRef": "showVatIdForEU" } ] } This stays clean and declarative. const adminOnlyHandler: VisibilityHandler = (_fieldName, valuesMap) => { return valuesMap['role'] === 'admin' ? 'visible' : 'invisible'; }; registerVisibility('adminOnly', adminOnlyHandler); const handleSubmit: FormSubmissionHandler = (_def, _instanceName, values, _t) => { alert(JSON.stringify(values, null, 2)); return undefined; // no errors → form submitted successfully }; registerSubmitter('alert', handleSubmit); This creates a clean separation: Without registries, you’re forced to embed logic somewhere it doesn’t belong. Registries give you a third option: keep logic in code, but reference it declaratively. Once definition and behavior were separated, another question appeared: Something has to: That responsibility doesn’t belong to the schema. Think of the runtime as a small execution engine. The runtime updates form state It evaluates visibility rules via the registry It triggers validation It updates derived state It notifies the renderer The renderer doesn’t make decisions. The system naturally settled into three layers: Definition Runtime Renderer Definition (JSON) | v Runtime Engine / | State Logic (Validation. Submission, visibility,...) | Registries | v Renderer (React/Vue/etc) This changes how you think about framework support. The biggest change wasn’t technical. Before: After: That shift is the real story behind Formitiva. I built Formitiva to reflect this model: The goal wasn’t just flexibility. Forms look like a UI problem. Once you separate definition, behavior, and rendering, things start to fall into place: That separation is what made the system finally feel stable. GitHub: https://github.com/Formitiva/formitiva-monorepo npm: Core: https://www.npmjs.com/package/@formitiva/core React: https://www.npmjs.com/package/@formitiva/react Vue: https://www.npmjs.com/package/@formitiva/vue Angular: https://www.npmjs.com/package/@formitiva/angular Vanilla: https://www.npmjs.com/package/@formitiva/vanilla If you’re building dynamic or backend-driven forms, I’d be curious: What mental shift changed how you approach them?