跪拜 Guibai
← Back to the summary

Functional vs Object-Oriented Frontend Architecture: Two Cost Curves

Functional vs Object-Oriented: Two Cost Curves for Frontend Architecture

Abstract: People habitually draw the battle line between React/Vue and Zova as “functional vs object-oriented,” but React’s Fiber nodes and Vue’s component instances reveal a fact: the so-called functional style hides an object system underneath. The real watershed is not syntax but who owns state identity, scope, and lifecycle: closures, or container-managed beans. This article re-reads the paradigm debate with two cost curves: functional style pays “architecture entropy” later, object style pays “ceremony” first. It also lists three hard requirements that only containerization solves cleanly (SSR request scope, interceptable call points, cross-module contracts), and three costs object style must honestly face. The final criterion is one sentence: what needs to be found, replaced, shared, and destroyed is an object; otherwise, let it be a function.

1. Dismantle a false opposition

When discussing the differences among Vue/React and Zova, the most common framing is “functional vs object-oriented.” The framing is convenient, but it mixes three different pairs together:

A single step into the source code shows that the classification “React is functional, Vue is functional, Zova is object-oriented” does not hold.

React’s “function” is only 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 to hold the hooks linked list. Pure functions need no identity, but React components have identity, a schedule, and a side-effect cleanup queue. React hides an object system under function syntax.

Vue’s setup() is a function, but each component instance is an object. The reactive host is a Proxy object, ref is a box with getters and setters, and reactive is a proxied object graph. Vue has never pretended to be purely functional.

So the real dividing line is not “whether functions are used”:

Who owns identity, scope, and lifecycle: closures, or container-managed beans?

This is the boundary between Zova and the default mental model of Vue/React.

2. Each model’s “default answer”

The same counter, written two ways, looks like a purely syntactic difference:

// Vue 3 default mental model: explicit reactive primitives
const count = ref(0);
const count2 = computed(() => `=== ${count.value} ===`);
function increment() {
  count.value++;
}
// Zova: 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 writing fewer .values. The difference is that in the first snippet, the host of state is the closure inside setup(), its lifecycle hangs on the component instance, and sharing requires a separate mechanism. In the second snippet, the host of state is a bean instance, and that instance belongs by nature to a certain container and a certain scope.

This one difference keeps showing itself as the system grows.

3. Cost curves: pay first or pay later

The real difference between the two models is where the costs appear.

Functional style: architecture entropy, deferred cost

A single component has almost no extra cost: write a function, declare a few refs, done. But Vue officially offers four mutually orthogonal sharing mechanisms:

When a system grows to dozens of modules and multiple teams, all four mechanisms exist simultaneously in one codebase. Which mechanism should a given piece of state use? There is no enforced answer; only team conventions. The density of conventions grows linearly with headcount, and deviations in execution grow exponentially with headcount. This is architecture entropy: it does not appear on day one, but it is never absent.

Object style: ceremony, prepaid cost

In Zova, every bean must declare its own role, scope, and lifecycle. Even a piece of state that only serves a single component has to answer “which bean are you, and which container do you live in?” first. This is prepaid ceremony.

What it buys is that sharing mechanisms converge into a single set:

class ControllerPage {
  @Use()
  $$localCounterState: CounterState;          // ctx container: component-instance level

  @Use({ injectionScope: 'host' })
  $$hostCounterState: CounterState;           // hierarchical lookup: parent-child sharing

  @Use({ injectionScope: 'app' })
  $$appCounterState: CounterState;            // app container: app/request level

  @Use({ injectionScope: 'sys' })
  $$sysCounterState: CounterState;            // sys container: system level
}

Inside a component, between components, across the application, across the system: four sharing scopes, one set of IoC semantics. provide/inject is no longer a separate subsystem; it is a natural corollary of the host / skipSelf injection scopes.

So “which is better” is a false question. The right question is: which layer does your system’s complexity mostly sit on? If it sits on “rendering composition,” functional flexibility is pure gain. If it sits on “domain ownership, modularity, long-term evolution,” object-style uniformity is what pays off.

4. Three things only containerization does cleanly

Some things are not a matter of “style choice” but of “whether the mechanism exists.”

1. SSR request scope

SSR has a hard constraint: state must be isolated across requests, and singleton module state leaks user data.

In the functional model, there is no addressable container called “request.” You have two options: module-level singletons (which leak), or rebuilding the entire module graph on every request (expensive and easy to miss). Zova’s app container is naturally the request boundary, and the sys container naturally spans requests. This is not syntactic sugar; a first-class SSR requirement is modeled directly into the architecture.

2. Interceptable call points

AOP, permission injection, logging, behavior extension: all of these require a call point to have a stable identity. A pure function call has no identity; to intercept it, you have to wrap functions layer by layer, and the wrapping layers themselves are anonymous and unaddressable. Bean method calls naturally pass through the container, and the container is naturally the interception point.

3. Cross-module contracts

class ControllerLayout {
  @Use('a-routertabs.model.tabs')
  $$modelTabs: ModelTabs;
}

The bean identifier lets the consumer depend on a contract name, not the provider’s file path. Modules can be reorganized, relocated, or have their implementations swapped without the consumer changing.

Functional cross-module sharing eventually converges on some global store key or context key, which is also essentially a string contract, except there is no container managing its registration, resolution, and lifecycle. Anyone can build such a mechanism; the difference is whether it is part of the architecture or part of the scaffolding.

5. Honestly listing the three costs of object style

If you only see the benefits, that is marketing, not architecture. Object style has three real costs:

1. Decorators plus metadata equals runtime magic. Behind @Use() is a collusion of build-time transformation and runtime resolution. Static readability drops, call stacks get deeper, and debugging has to pierce one more framework layer.

2. Granularity is forced coarser. When “everything should be a bean” becomes inertia, you get over-modeling where even an input box has to establish a role and a scope. 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 beans are non-serializable live object graphs. Zova can only grow on Vue, not React. This is not an aesthetic choice; the runtime foundation decides it. Vue’s component instance is already an object with a reactive scope, naturally suited to be the host of beans. React’s Fiber foundation is moving in the opposite direction.

6. Convergence point: objects own identity, functions own expression

Interestingly, both camps are moving toward each other’s core territory.

Zova itself is a hybrid: underneath it runs Vue reactivity, and $computed and $watch are thoroughly functional APIs. Objects take on identity, scope, and lifecycle; they do not take on computation.

The React ecosystem is also moving toward identity: the revival of signals, external state containers like QueryClient, and stores like Zustand that “take state out of closures” are all admissions that pure closures cannot carry all state and that an addressable host is needed.

That yields an actionable criterion:

If something needs to be “found, replaced, shared across scopes, or destroyed with its lifecycle,” it is an object; otherwise it should be a function.

Wrapping pure functions in objects is overdesign; using closures to carry things that need identity is architecture debt.

7. Conclusion: an argument on different abstraction levels

Finally, back to the comparison in the title.

Functional style solves “how to compose rendering”; 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 level. Directly comparing how elegant they are tends to produce the conclusion that “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 always worth asking:

Zova chooses to pay ceremony first and collect order later. Whether that choice is right does not depend on syntactic elegance; it depends on how long your system intends to live and how large it intends to grow.

Further reading

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

卷土的土 1 likes

State attribution matters more than syntactic form.