Reactive State
Microtask Scheduler & nextTick Utility
Section titled “Microtask Scheduler & nextTick Utility”Because state mutations are batched asynchronously, DOM updates do not happen immediately upon state assignment. If you inspect DOM dimensions or query rendered elements immediately after modifying this.state, you will read pre-update DOM measurements.
The nextTick utility function allows you to execute callbacks or await Promises immediately after the scheduler finishes flushing pending DOM updates.
Usage Variants
Section titled “Usage Variants”1. Component Instance Method (this.nextTick)
Section titled “1. Component Instance Method (this.nextTick)”Inside component actions, methods, or lifecycle hooks, use this.nextTick():
// Callback usagethis.state.items.push(newItem);this.nextTick(() => { const lastItem = this.$element.querySelector('li:last-child'); console.log('New item offsetHeight:', lastItem.offsetHeight);});
// Promise / Async-Await usageasync function addItem() { this.state.showModal = true; await this.nextTick(); const inputEl = this.$element.querySelector('.modal input'); inputEl.focus();}2. Framework Import (nextTick)
Section titled “2. Framework Import (nextTick)”Import nextTick directly from avenx-core/runtime when working outside component instance methods:
import { nextTick } from 'avenx-core/runtime';
component.state.title = 'Updated Title';await nextTick();console.log(document.title);Scheduler Architecture & Execution Order (scheduler.js)
Section titled “Scheduler Architecture & Execution Order (scheduler.js)”Avenx-JS manages asynchronous rendering using an internal microtask scheduler (lib/core/reactive/scheduler.js).
State Mutation │ ▼queueJob(job) ──► Deduplicate & push to job queue │ ▼queueFlush() ──► Schedule microtask (Promise.resolve().then(...)) │ ▼ flushJobs() ├─ 1. Sort job queue by component UID ascending (Parent before Child) ├─ 2. Execute DOM patch jobs └─ 3. Drain & execute flushCallbacks (nextTick callbacks)- Job Queueing & Deduplication (
queueJob): When a reactive state field mutates, the component’s update job (#updateJob) is pushed to the queue. Multiple mutations to the same component are deduplicated. - Microtask Deferred Execution (
queueFlush): The scheduler defers execution to a microtask using chained promises (Promise.resolve().then(() => Promise.resolve().then(flushJobs))). - Hierarchical Component Ordering: Before executing jobs in
flushJobs(), the scheduler sorts the queue ascending by componentid(UID). This guarantees that parent components re-render and patch the DOM before child components, preventing redundant updates or orphaned child renders. - Flush Callback Phase: After all DOM patch jobs finish, the scheduler drains and executes
flushCallbacks(includingnextTickcallbacks). If anextTickcallback mutates reactive state again, the scheduler recursively re-flushes until all queues are empty.
State Mutation ↓Proxy Interception ↓Scheduler Job Queue ↓Microtask Flush ↓DOM Patch ↓Slot Re-fill ↓onUpdate ExecutionBecause updates are queued and processed asynchronously, multiple synchronous state mutations can be grouped into a single rendering cycle instead of causing repeated DOM updates.
Troubleshooting AVX_R11
Section titled “Troubleshooting AVX_R11”Troubleshooting AVX_W09
Section titled “Troubleshooting AVX_W09”The AVX_W09 (ROUTE_PARAM_DECODE_FAILED) warning occurs when Avenx-JS cannot decode a route parameter because it contains malformed percent-encoding.
This warning is typically raised during route changes when parameters are extracted from the URL and decoded using JavaScript’s decodeURIComponent(). If decoding fails because the URI is malformed, Avenx-JS logs the warning instead of crashing the application.
For example, the following route parameter contains invalid percent-encoding:
The %2 sequence is incomplete and cannot be decoded.
A correctly encoded route would be:
where %20 represents a space.
To prevent this warning:
- Always encode route parameters using
encodeURIComponent()before constructing URLs. - Ensure every
%is followed by exactly two hexadecimal digits (0-9,A-F, ora-f). - Avoid manually writing encoded URL values whenever possible.
Example:
const userName = "John Doe";const url = `/profile/${encodeURIComponent(userName)}`;Common examples of percent encoding:
Valid
%20%2F%3AInvalid
%%2%ZZTroubleshooting AVX_W11
Section titled “Troubleshooting AVX_W11”The AVX_W11 (ROUTE_TITLE_EVALUATION_FAILED) warning occurs when a dynamic route title function throws an error while evaluating the route parameters.
For example, this route can trigger the warning if params.id is accessed through code that throws an error:
app.initRouter({ '/profile/:id': { page: 'Profile', title: (params) => getProfileTitle(params.id), },});The `AVX_R11` (`STATE_MUTATION_IN_UPDATE`) error occurs when state is mutated synchronously while Avenx-JS is already processing an update.
This can happen when state is modified from code that runs as part of rendering, such as a computed property or template expression. Updating state during this phase can schedule another update before the current update has finished, potentially creating an infinite rendering loop.
For example, avoid mutating state while computing a value:
```javascriptget displayName() { state.name = state.name.trim(); // Avoid: mutates state during an update return state.name;}Instead, computed getters should derive and return values without modifying state:
get displayName() { return state.name.trim();}If a state mutation must happen after the current update cycle has completed, defer it using setTimeout:
setTimeout(() => { state.name = state.name.trim();}, 0);Deferring the mutation allows the current rendering cycle to finish before another state update is scheduled.
When troubleshooting AVX_R11, check for state mutations inside computed getters, template expressions, or other code that executes during rendering. Prefer deriving values without side effects, and defer necessary state changes until after the current update cycle.
Nested Reactivity
Section titled “Nested Reactivity”Avenx-JS automatically intercepts nested object mutations. If a state property contains an array or object, mutations within that tree are tracked:
state.todos.push({ text: 'Learn Avenx', done: false }); // Reactive!state.user.profile.age = 35; // Reactive!Watchers & Advanced Options ($watch)
Section titled “Watchers & Advanced Options ($watch)”Watchers allow components to run side effects (such as making API calls, persisting values to localStorage, or manipulating DOM elements) in response to reactive state changes.
In Avenx-JS, watchers are registered using this.$watch(source, callback, options).
Watcher Method Signature
Section titled “Watcher Method Signature”this.$watch(source, callback, options)source: Dot-separated string path (e.g.'user.settings.theme') or getter function() => this.state.searchQuery.callback: Function called when the watched value changes(newValue, oldValue) => { ... }.options: Object specifying configuration options (immediate,deep,flush).
Advanced Options (options)
Section titled “Advanced Options (options)”1. Immediate Execution (immediate: true)
Section titled “1. Immediate Execution (immediate: true)”By default, watcher callbacks run only when the watched property changes after watcher registration. Set immediate: true to invoke the callback immediately upon creation with the current value (oldValue will be undefined):
// Triggers immediately with current searchQuery, then on subsequent changesthis.$watch('searchQuery', (newQuery) => { this.performSearch(newQuery);}, { immediate: true });2. Deep Tracking (deep: true)
Section titled “2. Deep Tracking (deep: true)”By default, string path watchers track shallow property replacements. Set deep: true to recursively observe nested object property mutations and array modifications:
// Fires when any property inside state.user.settings changesthis.$watch('user.settings', (newSettings) => { this.saveSettingsToLocalStorage(newSettings);}, { deep: true });3. Execution Timing (flush: 'pre' | 'post' | 'sync')
Section titled “3. Execution Timing (flush: 'pre' | 'post' | 'sync')”The flush option controls when the watcher callback is executed relative to the component’s DOM patch lifecycle:
| Value | Timing & Behavior | Common Use Cases |
|---|---|---|
'pre' (Default) |
Fires before DOM patch rendering takes place. | Preparing state calculations or computing secondary values before render. |
'post' |
Fires after DOM patch update completes. | Accessing updated DOM element measurements, scroll positions, or canvas elements. |
'sync' |
Fires synchronously immediately upon state mutation. | Real-time validation or synchronizing state with external non-DOM stores. |
// 'post' flush: container scroll position updated after DOM list re-rendersthis.$watch('messages.length', () => { const listEl = this.el.querySelector('.chat-messages'); listEl.scrollTop = listEl.scrollHeight;}, { flush: 'post' });Reactivity Injection (Provide / Inject)
Section titled “Reactivity Injection (Provide / Inject)”For deeply nested component trees, passing data down through props at every level (“prop drilling”) gets unwieldy. Avenx-JS offers a lighter-weight alternative to global bridges for this specific case: an ancestor component can provide values, and any descendant, no matter how deeply nested, can inject them directly — without the value passing through, or being known by, the components in between.
Unlike bridges, provide/inject is scoped to a single component subtree rather than the whole application, and it doesn’t route through the global bridge/render system, avoiding that overhead for state that’s only relevant to one part of the tree.
Providing values
Section titled “Providing values”Declare a provide property (or static method) on the ancestor component. It can be:
- An object, mapping keys to values or methods
- A function (instance or static) returning either form above, evaluated once per instance
- An array of keys, exposing matching properties already present on the component’s own
state,props, methods, or bridges
<state theme="dark" />;
// Object form: explicit keys and valuesprovide = { theme: this.state.theme, setTheme: (value) => { this.state.theme = value; },};// Array form: re-exposes existing state/props/methods by nameprovide = ['theme', 'setTheme'];Injecting values
Section titled “Injecting values”Descendant components declare inject the same way — object, function, or array of keys — and the resolved keys become directly accessible as properties on this (and inside template expressions):
inject = ['theme', 'setTheme'];
<button @click="setTheme(theme === 'dark' ? 'light' : 'dark')"> Current theme: {{ theme }}</button>To expose a provided value under a different local name, use the object form of inject, mapping the local key to the key it was provided under:
inject = { currentTheme: 'theme', // accessible as `this.currentTheme` / `{{ currentTheme }}`};How resolution works
Section titled “How resolution works”An injected key is resolved lazily, on every access — it is not copied or cached at mount time. When a descendant reads an injected property, Avenx walks up the DOM tree from the component’s root element to find the nearest ancestor component whose provide declares that key, then reads the current value from it.
This has two practical implications:
- Object-form
provideis reactive. The object passed toprovideis wrapped in its own reactive proxy internally. Injecting descendants read through that proxy on every access, so they automatically see updates when the provider changes a provided value — no extra wiring required. - Array-form
providestays reactive too, since it reads the provided key directly off the provider’s livestate/props/methods each time, rather than a snapshot.
Reactivity Exclusions and Limitations
Section titled “Reactivity Exclusions and Limitations”Avenx-JS uses JavaScript Proxy objects to track changes to reactive state. While this works well for plain JavaScript objects and arrays, some values are intentionally excluded from reactive tracking to preserve native behavior and avoid prototype-related issues.
Untracked Types
Section titled “Untracked Types”The following values are not automatically tracked by the reactivity system:
| Type | Reason |
|---|---|
Symbol properties |
Symbol keys are ignored during reactive tracking. |
Date instances |
Native class instances are not proxied. |
RegExp instances |
Regular expression objects are excluded from tracking. |
Map |
Internal mutations (set, delete, clear) are not observed. |
Set |
Internal mutations (add, delete, clear) are not observed. |
Frozen objects (Object.freeze) |
Frozen objects cannot be wrapped or mutated reactively. |
| Other built-in class instances | Native objects are intentionally excluded to preserve their original behavior. |
Why These Types Are Excluded
Section titled “Why These Types Are Excluded”These exclusions help:
- preserve the behavior of native JavaScript objects
- avoid prototype pollution
- prevent unexpected side effects when wrapping built-in objects
- keep the reactivity system predictable
Recommended Alternatives
Section titled “Recommended Alternatives”When possible, store plain JavaScript values inside reactive state instead of native class instances.
For example, instead of storing a Date object directly:
state.createdAt = new Date();store a primitive representation:
state.createdAt = Date.now();or
state.createdAt = new Date().toISOString();Instead of storing a Map:
state.users = new Map();consider using a plain object:
state.users = { alice: { role: "admin" }, bob: { role: "editor" }};or an array of entries:
state.users = [ { id: 1, name: "Alice" }, { id: 2, name: "Bob" }];Working with Non-Reactive Objects
Section titled “Working with Non-Reactive Objects”If your application needs to use native objects such as Map, Set, or custom class instances, consider storing a primitive representation in reactive state and recreating the object when needed.
For scenarios where external objects change independently of reactive state, update a tracked state property or use your application’s refresh mechanism to trigger a UI update after modifying the object.
Summary
Section titled “Summary”For the best reactive experience:
- ✅ Prefer plain objects and arrays.
- ✅ Store primitive values such as strings, numbers, and booleans.
- ✅ Convert native objects to serializable formats when appropriate.
- ❌ Do not rely on mutations of
Date,Map,Set,RegExp,Symbolproperties, or frozen objects to trigger UI updates.
Debugging Reactivity (debugReactivity)
Section titled “Debugging Reactivity (debugReactivity)”During development, tracing dependency graphs and understanding why a component re-rendered or why a watcher triggered can be tricky. Avenx-JS includes a reactivity tracing engine (lib/core/reactive/watcher.js) that logs detailed dependency registration and update events directly to the browser DevTools console.
Enabling Tracing
Section titled “Enabling Tracing”Reactivity debugging can be enabled through three different mechanisms:
1. Build Configuration (avenx.config.json)
Section titled “1. Build Configuration (avenx.config.json)”Enable reactivity logging project-wide by setting debug.debugReactivity to true:
{ "debug": { "debugReactivity": true }}2. Programmatic Runtime API (setDebugReactivity)
Section titled “2. Programmatic Runtime API (setDebugReactivity)”Enable or disable reactivity debugging dynamically in your code using setDebugReactivity:
import { setDebugReactivity, isDebugReactivityEnabled } from 'avenx-core/runtime';
// Enable reactivity tracing programmaticallysetDebugReactivity(true);
console.log('Reactivity tracing active:', isDebugReactivityEnabled()); // true3. Dynamic Browser Console Flag (window.__avenx_debug_reactivity__)
Section titled “3. Dynamic Browser Console Flag (window.__avenx_debug_reactivity__)”Toggle reactivity logging on the fly inside the browser DevTools console without restarting your application:
// Enable in browser DevToolswindow.__avenx_debug_reactivity__ = true;
// Disable when finished debuggingwindow.__avenx_debug_reactivity__ = false;What Gets Logged
Section titled “What Gets Logged”When reactivity tracing is enabled, Avenx-JS outputs structured log messages:
- Dependency Tracking: Logs whenever an
AvenxWatcheraccesses a Proxy property and registers a reactive dependency. - State Mutations: Logs property modifications and Proxy traps (e.g. state mutations and value updates).
- Watcher Invalidation: Logs when dirty state notifications queue a watcher re-render job.