Appearance
Zova Reactivity Under the Hood
This guide explains the source-level runtime path behind Zova’s reactive page and component authoring model.
Use this page after Reading Zova for Vue Developers when you want to move from the public mental model to the core source path that makes plain controller fields behave reactively.
Why this page exists
The public frontend docs already explain the architectural intent:
- Zova keeps Vue 3 reactive strengths
- Zova prefers controller-oriented and bean-oriented authoring
- IoC and bean scopes organize state and behavior sharing
What many source readers still want next is the implementation bridge:
- where the controller instance is created
- where it becomes reactive
- where
$computedconnects back to Vuecomputed - where page route state is pushed onto the controller
- where controller-driven render logic joins the normal update flow
This page is that bridge.
The shortest accurate runtime model
For a page controller, the shortest accurate model is:
useControllerPage(...)creates the Zova context and controller load path- the bean container creates the controller bean with
markReactive = true - the bean container wraps the controller instance with Vue
reactive(...) - bean helpers such as
$computed()and$watch()wrap Vue reactive APIs inside the controller instance scope - the component runtime patches render so the controller or render bean becomes the effective render entry
- before each render, page-controller route data can be refreshed onto the controller surface
- when a controller field changes, Vue dependency tracking drives recomputation and rerender
That is why Zova can offer direct-looking class-field authoring while still using Vue runtime behavior underneath.
A concrete source specimen
A small public example that shows the pattern clearly is:
text
zova/src/suite/a-demo/modules/demo-basic/src/page/state/controller.tsxRepresentative shape:
typescript
@Controller()
export class ControllerPageState extends BeanControllerPageBase {
count: number = 0;
count2: string;
protected async __init__() {
this.count2 = this.$computed(() => {
return `=== ${this.count} ===`;
});
}
increment() {
this.count++;
}
protected render() {
return <div>{this.count2}</div>;
}
}What matters here is not only the syntax.
What matters is that:
countis a plain class fieldcount2is instance-level derived stateincrement()mutates the controller instance directlyrender()reads controller fields directly
The rest of this page explains how that becomes real runtime reactivity.
The core source-reading path
When you want to trace the implementation, read these files in order:
zova/packages-zova/zova-core/src/composables/useController.tszova/packages-zova/zova-core/src/bean/beanContainer.tszova/packages-zova/zova-core/src/bean/beanBase.tszova/packages-zova/zova-core/src/bean/beanControllerPageBase.tszova/packages-zova/zova-core/src/core/context/component.tszova/src/suite-vendor/a-zova/modules/a-router/src/monkey.ts
A compact role map is:
useController.tscreates and loads controller, style, and render beansbeanContainer.tsinstantiates beans, injects context, and appliesreactive(...)beanBase.tsexposes instance-scoped wrappers around Vue helpers such ascomputedandwatchbeanControllerPageBase.tsprovides the page-controller hook points for route-aware controller data updatescomponent.tsredirects component render toward controller-oriented render flowa-router/src/monkey.tspushes page route state onto page controllers
Step-by-step runtime path
1. useControllerPage(...) starts the controller load path
The public entrypoint for a page is useControllerPage(...) in:
text
zova/packages-zova/zova-core/src/composables/useController.tsThe important job here is not only creating a helper.
It creates the Zova context, prepares controller data, and registers the async load path that will create the controller bean and related beans.
A practical reading takeaway is:
- the page controller is not an ad hoc class instance created by page code
- it is created by the framework runtime inside the bean/container model
2. The controller bean is created with markReactive = true
Inside the controller load path, the framework calls the bean container to create the controller bean with reactive wrapping enabled.
That is the key bridge between:
- plain class authoring in user code
- real Vue reactive behavior at runtime
A practical reading takeaway is:
- the framework decides that the controller instance itself is a reactive host
3. The bean container wraps the instance with Vue reactive(...)
In:
text
zova/packages-zova/zova-core/src/bean/beanContainer.tsThe bean container:
- creates the class instance
- attaches framework context such as
sys,app, andctx - records bean identity metadata
- applies Vue
reactive(...)when the bean should be reactive - may also patch the bean with AOP-oriented proxy behavior before the final reactive wrapper is exposed
This is one of the most important source-level facts for understanding Zova.
Zova does not require the business author to write reactive({ ... }) around controller state, because the framework already treats the controller bean as the reactive object.
Construction-time this and the exposed reactive bean
There is one lifecycle boundary that matters when a callback closes over this:
- the container first constructs the raw class instance with
new BeanClass(...); - it then exposes the framework-managed reactive/proxied bean; and
- it invokes
__init__()only after that preparation completes.
That distinction is normally invisible when a controller uses ordinary methods and render-time this.member access. It matters for a class-field arrow callback that mutates controller state, because the arrow is created during construction and lexically captures the construction-time raw this:
typescript
class ControllerPageCounter {
count = 0;
// Avoid for a state-mutating controller callback.
onIncrement = () => {
this.count++;
};
}The field can change on the raw instance, but the mutation bypasses the reactive/proxied bean through which render dependencies were collected. Vue therefore receives no invalidation notification. A later update to another dependency can rerender the component and reveal the changed underlying value, which can make this look intermittent.
Prefer an ordinary controller method for a normal TSX action:
typescript
class ControllerPageCounter {
count = 0;
increment() {
this.count++;
}
protected render() {
return <button onClick={() => this.increment()}>Increment</button>;
}
}If an external API requires a stable stored callback, create that closure in __init__() instead. At that point, its lexical this is the framework-exposed reactive/proxied bean:
typescript
class ControllerPageCounter {
count = 0;
onIncrement: () => void;
protected async __init__() {
this.onIncrement = () => {
this.count++;
};
}
}This is not a rule that arrow functions are generally non-reactive. The narrow hazard is a class-field arrow callback that captures construction-time this and mutates bean state.
4. $computed() is an instance-scoped wrapper around Vue computed(...)
In:
text
zova/packages-zova/zova-core/src/bean/beanBase.tsHelpers such as these are exposed from the bean surface:
$computed()$watch()$watchEffect()$watchPostEffect()$watchSyncEffect()$toRef()$customRef()
The important reading point is:
- Zova is not replacing Vue
computedwith a different reactive theory - Zova is exposing Vue’s reactive capabilities through a bean-oriented, instance-scoped API surface
So when a controller writes:
typescript
this.count2 = this.$computed(() => {
return `=== ${this.count} ===`;
});what really happens is still dependency tracking through Vue’s computed system.
5. Page-controller route data uses a controller-data update path
In:
text
zova/packages-zova/zova-core/src/bean/beanControllerPageBase.tspage controllers expose page-oriented controller data refresh hooks.
Then in:
text
zova/src/suite-vendor/a-zova/modules/a-router/src/monkey.tsroute-aware controller data is prepared, initialized, and updated.
That is where page-controller members such as these are maintained:
$route$params$query
A practical reading takeaway is:
- page route state is not only pulled ad hoc by the page
- the framework pushes and refreshes route-aware controller data as part of the page-controller runtime path
This is one of the biggest differences a Vue-first reader usually notices.
6. The component runtime patches render toward the controller/render bean path
In:
text
zova/packages-zova/zova-core/src/core/context/component.tsZova patches the component instance render method so the effective render flow becomes controller-oriented.
That is why the framework can support these patterns consistently:
- render directly inside the controller
- split render into a dedicated render bean later
- keep the higher-level page/component authoring model stable while the implementation grows
A practical reading takeaway is:
- render is not treated as only a local component-file concern
- render is part of the controller/bean architecture
7. Field mutation becomes normal reactive invalidation and rerender
Once render has read fields through the framework-exposed reactive/proxied controller, a mutation through that same identity, such as:
typescript
this.count++;behaves the way a Vue reader would expect at the reactive-engine level:
- the field change invalidates dependencies
- computed values depending on that field are recomputed
- the next render sees the updated values
The construction-time class-field arrow case described above is the exception: its raw this can change the underlying field without triggering the dependency that render collected through the exposed bean.
So the runtime behavior is still recognizably Vue-like. The architectural surface, including the bean lifecycle and identity boundary, is what changed.
A compact call-flow sketch
text
useControllerPage(...)
-> _useController(...)
-> ctx.bean._newBeanInner(..., markReactive = true)
-> BeanContainer constructs the raw controller bean
-> BeanContainer exposes the reactive/proxied controller bean
-> controller __init__ wires $computed / $watch helpers and any stored callbacks
-> component render is patched toward controller/render bean
-> render-time controller data update refreshes page route data
-> render reads fields through the exposed bean
-> mutation through that same identity invalidates dependencies
-> rerender produces updated UIUse this sketch as the durable mental model even if lower-level implementation details evolve over time.
What is stable vs what is implementation detail
Stable public architectural idea
The following ideas are durable and should guide how you read and extend Zova code:
- controller and bean instances are first-class authoring surfaces
- Vue reactivity is still the foundation underneath
- bean lifecycle and IoC scope are central to the architecture
- render flow is controller-oriented
- route-aware page state is part of the page-controller surface
More implementation-shaped detail
The following details are helpful for source reading, but are not the main public design lesson:
- the exact internal helper names
- the exact order of intermediate load helpers
- the exact proxy or monkey hook boundaries used by the runtime
- the exact render patch mechanics inside the component context implementation
This distinction matters because public docs should teach the durable model first, then use source details only to clarify that model.
When to use this page
Use this page when:
- you already understand the public Zova architecture but want to trace the runtime source path
- you need to debug why a page controller field updates the UI
- you need to understand where route state is refreshed before render
- you are deciding whether a change belongs in controller authoring, bean lifecycle, route synchronization, or render-flow plumbing
If you are still learning the public mental model first, start with:
Final takeaway
The most important source-reading insight is simple:
Zova does not remove Vue reactivity. It relocates the business-facing reactive surface from local reactive primitives toward framework-managed reactive bean instances.
Once that clicks, the rest of the controller, render-bean, route, and lifecycle design becomes much easier to follow.