Technology Apr 30, 2026 · 6 min read

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

From UI Components to Runtime Architecture: The shift that fixed how I build forms at scale. I used to think forms were just UI. And to be fair, that belief works—for a while. You open a React component, add a few inputs, wire validation, handle submit, and ship it. The form works. Nothing feels w...

DE
DEV Community
by yanggmtl
Forms Aren’t UI: The Architecture Mistake Most React Apps Make

From UI Components to Runtime Architecture: The shift that fixed how I build forms at scale.

I used to think forms were just UI.
And to be fair, that belief works—for a while.

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.
Then the product started to scale.
And everything that felt simple started to break.

When “Just UI” Stops Working

The first few forms were fine.
The next few were manageable.

But over time, patterns started repeating—and so did the friction.
One team needed a slightly different onboarding flow.
Another needed conditional fields.
Another tenant required different validation rules.
Another product wanted the backend to define part of the form.

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”.

My First Wrong Assumption

I thought I had a React problem.
So I did what most frontend engineers do:

  • extracted reusable components
  • added hooks
  • built better abstractions
  • reorganized the component tree

But none of that fixed the core issue.
Because the real problem wasn’t the component layer.

The problem was that the form definition itself was trapped inside the UI.

The First Shift: Forms Are Not Screens

Once I looked closely, it became obvious.
If a form is hardcoded in React, then its definition is:

  • 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.
They are data.

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.
Until it wasn’t.

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.
It became a mini programming language.

And I ended up in a familiar place again—just in a different form.
The location of the complexity changed.
The complexity itself did not.

Instead of messy components, I had bloated schemas.
Instead of hardcoded UI logic, I had hardcoded configuration logic.

Dynamic forms didn’t solve the problem.
They just moved it.

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?
That question led to the architecture behind Formitiva.

The Key Insight: Separate the Concerns

Underneath everything, a form has three distinct concerns:

  1. What the form is (structure)
  2. How the form behaves (logic)
  3. How the form is rendered (UI)

Most systems mix these together.
That’s where things break.

The most important rule I arrived at was this:
Behavior logic must be separated from both the definition and the renderer.

Why Common Approaches Break

Without that separation, you usually end up choosing between two problematic options:

  1. Logic inside the schema
    • turns the schema into a mini programming Language
    • hard to maintain and debug
    • mixes data with execution

  2. Logic inside components
    • tightly couples behavior to UI
    • kills portability
    • hard to reuse across frameworks

Neither scales well.

The Idea That Unlocked It: Registries

The solution I landed on was simple but powerful:
Let the definition reference behavior, not implement it.

Example:

{
  "name": "signup",
  "submitterRef": "createAccount",
  "properties": [
    {
      "name": "vatId",
      "type": "text",
      "visibilityRef": "showVatIdForEU"
    }
  ]
}

This stays clean and declarative.
Then the actual logic lives in code:

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:
• the definition expresses intent
• the registries implement behavior

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.

The Next Shift: The Runtime Is the Brain

Once definition and behavior were separated, another question appeared:
Who actually runs the form?

Something has to:
• manage state
• evaluate conditions
• trigger validation
• resolve registry functions
• handle submission

That responsibility doesn’t belong to the schema.
And it shouldn’t belong to the renderer.
That’s where the runtime comes in.

What the Runtime Actually Does

Think of the runtime as a small execution engine.
For example, when a user updates a field:

  1. The runtime updates form state
  2. It evaluates visibility rules via the registry
  3. It triggers validation
  4. It updates derived state
  5. It notifies the renderer

The renderer doesn’t make decisions.
It just reflects state.
That separation is what keeps the system predictable.

The Architecture

The system naturally settled into three layers:

  1. Definition
    • structure and metadata
    • references to behavior
    • no executable logic

  2. Runtime
    • state management
    • lifecycle orchestration
    • registry resolution

  3. Renderer
    • UI output (React, Vue, Angular, Vanilla JS)
    • no business logic

        Definition (JSON)
              |
              v
        Runtime Engine
        /     | 
 State     Logic (Validation. Submission, visibility,...)
              |    
          Registries
              |
              v
        Renderer (React/Vue/etc)

This changes how you think about framework support.
Instead of rebuilding form logic per framework, you keep the runtime stable and let renderers sit at the edge.

What Actually Changed for Me

The biggest change wasn’t technical.
It was how I thought about the problem.

Before:
• how do I wire this field?
• where does this validation go?
• how do I reuse this component?

After:
• what belongs in the definition?
• what belongs in the runtime?
• what should be registered instead of embedded?
• how do I keep the renderer thin?

That shift is the real story behind Formitiva.

Why Formitiva Exists

I built Formitiva to reflect this model:
• forms defined as data
• behavior implemented via registries
• runtime as the execution layer
• renderers as interchangeable adapters

The goal wasn’t just flexibility.
It was honest separation of concerns.

Closing Thought

Forms look like a UI problem.
But at scale, they’re an architecture problem.

Once you separate definition, behavior, and rendering, things start to fall into place:
• definitions stay declarative
• logic stays modular
• runtimes stay coherent
• renderers stay replaceable

That separation is what made the system finally feel stable.

Links

  1. GitHub: https://github.com/Formitiva/formitiva-monorepo
  2. npm:

If you’re building dynamic or backend-driven forms, I’d be curious:
What mental shift changed how you approach them?

DE
Source

This article was originally published by DEV Community and written by yanggmtl.

Read original article on DEV Community
Back to Discover

Reading List