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:
- What the form is (structure)
- How the form behaves (logic)
- 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:
Logic inside the schema
• turns the schema into a mini programming Language
• hard to maintain and debug
• mixes data with executionLogic 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:
- 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.
It just reflects state.
That separation is what keeps the system predictable.
The Architecture
The system naturally settled into three layers:
Definition
• structure and metadata
• references to behavior
• no executable logicRuntime
• state management
• lifecycle orchestration
• registry resolutionRenderer
• 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
- GitHub: https://github.com/Formitiva/formitiva-monorepo
- npm:
If you’re building dynamic or backend-driven forms, I’d be curious:
What mental shift changed how you approach them?
Top comments (0)