Abstract: People habitually carve the React/Vue and Zova camps into "functional vs object-oriented." But React's Fiber nodes and Vue's component instances reveal a fact: beneath that so-called functional style, an object system is always hiding. The real dividing line is not syntax — it is who owns state identity, scope, and lifecycle: the closure, or a container-managed bean. This article re-reads the paradigm debate through two cost curves: the functional style pays "architectural entropy" afterwards, the object style pays "ceremony" up front. It then lists three hard requirements only containerization solves cleanly (SSR request scope, interceptable call sites, cross-module contracts), and three costs an object style must honestly face. The final criterion boils down to one sentence: if something needs to be found, replaced, shared, or disposed, make it an object; otherwise, let it be a function.
1. Tearing down a false dichotomy first
When people discuss the difference between Vue/React and Zova, the most common framing is "functional vs object-oriented." It's a convenient label, but it conflates three distinct things:
- Syntactic form: writing functions or classes
- State host: closure variables or object properties
- Architectural ownership: who manages identity, scope, and lifecycle
Take one step into the source code, and the classification "React is functional, Vue is functional, Zova is object-oriented" collapses.
React's "functions" are merely the expression form of rendering. Hooks depend on a fixed call order — which means every function component must have a persistent Fiber node behind it holding the hooks linked list. Pure functions need no identity; React components have identity, scheduling phases, and effect-cleanup queues. React hides an object system beneath function syntax.
Vue's setup() is a function, but every component instance is an object. The reactive host is a Proxy object, ref is a box with getter/setter, reactive is a proxied object graph. Vue never pretended to be purely functional.
So the real watershed is not "whether to use functions" — it is:
Who owns identity, scope, and lifecycle — the closure, or a container-managed bean?
That is the actual boundary between Zova's default mental model and Vue/React's.
2. Each model's "default answer"
The same counter, written two ways — the difference looks like mere syntax:
// Vue 3 default mental model: explicit reactive primitives
const count = ref(0);
const count2 = computed(() => `=== ${count.value} ===`);
function increment() {
count.value++;
}
// Zova: a framework-managed reactive object
export class Counter {
count: number = 0;
count2: string;
protected async __init__() {
this.count2 = this.$computed(() => `=== ${this.count} ===`);
}
increment() {
this.count++;
}
protected render() {
return (
<div>
<div>{this.count2}</div>
<button onClick={() => this.increment()}>Increment</button>
</div>
);
}
}
The difference is not that we saved a few .value keystrokes. In the first snippet, state's host is the setup() closure, lifecycle hangs off the component instance, and sharing requires hunting for another mechanism. In the second, state's host is a bean instance that inherently belongs to a container, to a scope.
This single distinction will reveal itself over and over as a system grows.
3. Cost curves: pre-paid and post-paid
The real divergence between the two models is where the cost shows up.
Functional style: architectural entropy, paid after
A single component costs almost nothing extra — write a function, declare a few refs, done. But Vue officially ships four mutually orthogonal sharing mechanisms:
- props / emits
- composables
provide / inject- store layer (Pinia, etc.)
When a system grows to dozens of modules and multiple teams, all four mechanisms coexist in one codebase. Which state should use which? There is no enforced answer, only team conventions. Convention density grows linearly with headcount, while execution drift grows exponentially. That is architectural entropy — it never shows up on day one, but it never fails to show up.
Object style: ceremony, paid up front
In Zova, every bean must declare its role, scope, and lifecycle. Even a state that only serves a single component's internals has to answer first: "which bean are you, and which container do you live in?" That is the up-front ceremony.
What it buys in return is the convergence of sharing into one mechanism:
class ControllerPage {
@Use()
$$localCounterState: CounterState; // ctx container: component-instance scope
@Use({ injectionScope: 'host' })
$$hostCounterState: CounterState; // hierarchical lookup: parent–child sharing
@Use({ injectionScope: 'app' })
$$appCounterState: CounterState; // app container: app / request scope
@Use({ injectionScope: 'sys' })
$$sysCounterState: CounterState; // sys container: system scope
}
Component-internal, between-components, app-global, system-global — four sharing scopes, one IoC semantics. provide/inject stops being an independent subsystem and becomes the natural corollary of host / skipSelf injection scopes.
So "which is better" is a pseudo-question. The right question is: where does your system's complexity primarily land? If it lands on "rendering composition," the functional style's flexibility is pure gain. If it lands on "domain ownership, modularity, long-term evolution," the object style's uniformity pays back.
4. Three things only containerization does cleanly
Some questions are not "style choices" — they are "does this mechanism exist?" questions.
1. SSR request scope
SSR has a hard constraint: state must be isolated across requests; module-level singletons leak user data.
In the functional model, there is no addressable container called "the request." You only have two choices: module-level singletons (leak) or rebuilding the entire module graph per request (expensive and error-prone). Zova's app container is natively the request boundary, and the sys container is natively cross-request — this is not syntax sugar; it is a first-class SSR requirement modeled directly into the architecture.
2. Interceptable call sites
AOP, permission injection, logging, behavior extension — all of these require "call sites" to have stable identity. Pure function calls have no identity; to intercept you have to wrap functions in layers, and the wrapper layers themselves are anonymous and unaddressable. A bean's method call naturally passes through the container, and the container is naturally the interception point.
3. Cross-module contracts
class ControllerLayout {
@Use('a-routertabs.model.tabs')
$$modelTabs: ModelTabs;
}
Bean identifiers let consumers depend on a contract name, not the provider's file path. Modules can be reorganized, relocated, have their implementations replaced — and consumers do not move.
Functional cross-module sharing eventually converges onto some global store key or context key — essentially a string contract too, except no container is managing its registration, resolution, and lifecycle for you. Anyone can reinvent the mechanism; the difference is whether it is part of the architecture or part of the scaffolding.
5. Honestly listing three costs of the object style
If you only see the benefits, that is marketing, not architecture. The object style carries three real costs:
1. Decorators + metadata = runtime magic. Behind @Use() is a conspiracy of build-time transforms and runtime resolution. Static readability drops, call stacks go deeper, and debugging means piercing one extra layer of framework.
2. Granularity gets forced coarser. When "everything should be a bean" becomes a habit, you end up with an input box that still needs a role and a scope — over-modeling. The object style needs discipline to fight its own ceremony.
3. Structural conflict with React's evolution direction. RSC and concurrent rendering push state toward serializable pure data, while a bean is a non-serializable live object graph. Zova can only grow on Vue, not on React — and this is not an aesthetic choice, it is determined by the runtime substrate: Vue's component instance is already an object with a reactive scope, naturally suited to host beans; React's Fiber substrate is heading in the opposite direction.
6. Convergence: objects own identity, functions own expression
The interesting thing is that both camps are moving into each other's core territory.
Zova itself is a hybrid: underneath runs Vue reactivity, and $computed and $watch are thoroughly functional APIs. Objects only carry identity, scope, and lifecycle — not computation.
The React ecosystem is also moving toward identity: the signals revival, external state containers like QueryClient, stores like Zustand that "pull state out of the closure" — all essentially admitting that pure closures cannot carry all state, and that an addressable host is needed.
From this we get an actionable judgment criterion:
If something needs to be "found, replaced, shared across scopes, disposed with a lifecycle," it is an object; otherwise it should be a function.
Wrapping a pure function in an object is over-design; using a closure to carry something that needs identity is architectural debt.
7. Conclusion: a debate not on the same abstraction layer
Let us return to the comparison in the title.
The functional style solves "how to compose rendering." The object style solves "how to organize a long-lived system." The former is a programming paradigm; the latter is an architectural paradigm — they are not on the same abstraction layer. Comparing their elegance directly often leads to "FP is more advanced," but that conclusion is wrong: React's hooks are precisely effectful closures with order constraints, side effects, and Fiber scheduling — they are no closer to pure functions than classes are.
The questions worth asking are always:
- In my system, who does state identity belong to?
- Is the state-sharing mechanism one set or four?
- Is the SSR request boundary modeled?
- When a newcomer takes over an unfamiliar module and needs to change some stateful behavior: do they have to rummage through composables, stores, and provide/inject one by one to find the state's host, or do they just ask "which bean is this?"
Zova chooses to pay ceremony up front and reap order later. Whether that choice is right depends not on how elegant the syntax is, but on how long your system plans to live and how big it plans to grow.
Further reading
- GitHub: https://github.com/cabloy/cabloy
- Docs: https://cabloy.com
- Demo: https://cabloy.com/demo
Top comments (0)