Shared State & Bridges
Bridges provide an elegant, lightweight solution for sharing state and business logic across multiple components or pages without prop-drilling.
Creating a Bridge
Section titled “Creating a Bridge”Generate a new bridge using the CLI tool:
npx avenx g bridge authThis creates a file in src/global/auth.bridge.js. The generated bridge uses the class-based pattern and extends the AvenxBridge base class:
import { AvenxBridge } from 'avenx-core/runtime';
export default class AuthBridge extends AvenxBridge { constructor() { super();
this.isLoggedIn = false; this.user = { name: 'Guest', role: 'visitor', }; }
logout() { this.isLoggedIn = false; this.user.name = 'Guest'; }}The AvenxBridge Base Class
Section titled “The AvenxBridge Base Class”The AvenxBridge base class provides the foundation for creating class-based bridges in Avenx-JS. Extending this class keeps bridge definitions consistent with Avenx’s object-oriented model and the bridge structure generated by the CLI.
The bridge constructor is responsible for initializing the shared state. Calling super() initializes the AvenxBridge base class before defining state properties on the bridge instance.
constructor() { super();
this.isLoggedIn = false; this.user = { name: 'Guest', role: 'visitor', };}Methods containing shared business logic can be defined directly on the bridge class. These methods can read and modify the bridge’s shared state using this.
Using Bridges in Components
Section titled “Using Bridges in Components”Bridges are automatically loaded and registered by the compiler. They are exposed directly to component templates and actions under their capitalized name postfixed with Bridge (e.g. AuthBridge).
<p>Current User: {{ AuthBridge.user.name }}</p>
<action name="login"> AuthBridge.isLoggedIn = true; AuthBridge.user.name = "John Doe"; </action>
<button @click="AuthBridge.logout()">Log Out</button>Plain Object Bridges
Section titled “Plain Object Bridges”Class-based bridges extending AvenxBridge are the primary approach and match the structure generated by the Avenx CLI. However, bridges can also be defined using plain JavaScript objects:
export default { isLoggedIn: false, user: { name: 'Guest', role: 'visitor', }, logout() { this.isLoggedIn = false; this.user.name = 'Guest'; },};Plain object bridges provide a simpler alternative for small amounts of shared state and business logic. For consistency with the CLI-generated structure and Avenx’s object-oriented model, the class-based AvenxBridge pattern is recommended as the primary approach.
Compilation Lifecycle & Limits
Section titled “Compilation Lifecycle & Limits”The Avenx compiler processes .bridge.js files by extracting the bridge definition beginning at export default. Declarations placed before export default, such as local variables, constants, and helper functions, are not preserved in the compiled output.
As a result, bridge code that depends on declarations defined above export default may cause runtime ReferenceError exceptions after compilation.
For example, avoid defining local utilities before the bridge export:
const defaultRole = 'visitor';
function createGuestUser() { return { name: 'Guest', role: defaultRole, };}
export default class AuthBridge extends AvenxBridge { constructor() { super(); this.user = createGuestUser(); }}In this example, defaultRole and createGuestUser are declared before export default and may be removed during compilation, leaving the bridge with references to declarations that no longer exist.
Instead, keep helper methods inside the exported bridge class when the logic belongs specifically to that bridge:
export default class AuthBridge extends AvenxBridge { constructor() { super(); this.user = this.createGuestUser(); }
createGuestUser() { return { name: 'Guest', role: 'visitor', }; }}For reusable utilities shared across multiple parts of an application, move the logic into external utility files and expose it through supported application patterns. Utilities that are intentionally available globally can also be referenced through properties on the window object.
export default class AuthBridge extends AvenxBridge { constructor() { super(); this.user = window.AppUtils.createGuestUser(); }}When writing .bridge.js files:
- Do not rely on variables, constants, or helper functions declared before
export default. - Keep bridge-specific helper methods inside the exported bridge class.
- Move reusable logic into external utility files.
- Reference intentionally global utilities through properties on
window.
Understanding these compilation limits helps prevent missing declarations and runtime ReferenceError exceptions caused by helper code being removed from the compiled output.
End-to-End Global Store Patterns
Section titled “End-to-End Global Store Patterns”The examples below show complete create → register → bind flows for common shared-state use cases. Each bridge is a single reactive source of truth; components that read its properties re-render when those properties change.
Theme Switching Store
Section titled “Theme Switching Store”import { AvenxBridge } from 'avenx-core/runtime';
export default class ThemeBridge extends AvenxBridge { constructor() { super(); this.mode = 'light'; // 'light' | 'dark' }
toggle() { this.mode = this.mode === 'light' ? 'dark' : 'light'; }
setMode(mode) { this.mode = mode === 'dark' ? 'dark' : 'light'; }}<!-- Any component template --><div class="shell" data-theme="{{ ThemeBridge.mode }}"> <button @click="ThemeBridge.toggle()"> Switch to {{ ThemeBridge.mode === 'light' ? 'dark' : 'light' }} mode </button></div>Updating ThemeBridge.mode (via toggle(), setMode(), or direct assignment) notifies every component that interpolated ThemeBridge.mode, so headers, pages, and widgets stay in sync without prop drilling.
Authentication Store
Section titled “Authentication Store”import { AvenxBridge } from 'avenx-core/runtime';
export default class AuthBridge extends AvenxBridge { constructor() { super(); this.isLoggedIn = false; this.token = null; this.user = { name: 'Guest', role: 'visitor' }; }
login(user, token) { this.isLoggedIn = true; this.token = token; this.user = user; }
logout() { this.isLoggedIn = false; this.token = null; this.user = { name: 'Guest', role: 'visitor' }; }}<!-- Navbar --><span data-ax-show="AuthBridge.isLoggedIn">Hello, {{ AuthBridge.user.name }}</span><button data-ax-show="AuthBridge.isLoggedIn" @click="AuthBridge.logout()">Log out</button>
<!-- Protected page action --><action name="save"> if (!AuthBridge.isLoggedIn) { return; } // proceed with authenticated request using AuthBridge.token</action>Shopping Cart Store
Section titled “Shopping Cart Store”import { AvenxBridge } from 'avenx-core/runtime';
export default class CartBridge extends AvenxBridge { constructor() { super(); this.items = []; }
get itemCount() { return this.items.reduce((sum, item) => sum + item.qty, 0); }
get subtotal() { return this.items.reduce((sum, item) => sum + item.price * item.qty, 0); }
addItem(product) { const existing = this.items.find((item) => item.id === product.id); if (existing) { existing.qty += 1; // Reassign the array so dependents observing `items` re-render reliably this.items = [...this.items]; return; } this.items = [...this.items, { ...product, qty: 1 }]; }
clear() { this.items = []; }}<!-- Header badge --><span class="cart-badge">{{ CartBridge.itemCount }}</span>
<!-- Cart page --><ul> <li data-ax-for="item in CartBridge.items"> {{ item.name }} × {{ item.qty }} </li></ul><p>Subtotal: {{ CartBridge.subtotal }}</p>Multi-Component Reactivity Flow
Section titled “Multi-Component Reactivity Flow”- The compiler discovers
*.bridge.jsfiles and registers each bridge on the app (typically asNameBridge). - A component template or action reads
SomeBridge.property— the runtime tracks that dependency. - Another component (or the bridge itself) mutates
SomeBridge.property. - Only components that depend on the changed paths schedule updates; unrelated trees are left alone.
You can also register bridges manually in tests or custom bootstraps:
import { AvenxApp } from 'avenx-core/runtime';import AuthBridge from './global/auth.bridge.js';
const app = new AvenxApp({ target: '#app' });app.registerBridge('AuthBridge', new AuthBridge());Best Practices
Section titled “Best Practices”Initialize state in the constructor
Section titled “Initialize state in the constructor”Set every shared field to a defined default in constructor() after super(). Avoid lazy undefined fields that first appear mid-session — they make template guards harder and can skip early dependency tracking.
Prefer narrow mutations over wholesale replacement
Section titled “Prefer narrow mutations over wholesale replacement”Mutate the specific property that changed (AuthBridge.user.name = 'Ada') when possible. Replacing large nested objects forces more dependents to re-evaluate. When you must replace arrays (for example after push-style edits that proxies may not always surface cleanly), assign a new array reference intentionally: this.items = [...this.items, next].
Avoid full-app re-render bottlenecks
Section titled “Avoid full-app re-render bottlenecks”- Keep hot paths (animation frames, pointer moves) out of bridge state; use local component state instead.
- Do not store derived UI flags that every layout shell reads if only one widget needs them.
- Split large domains into multiple bridges (
AuthBridge,CartBridge,ThemeBridge) rather than one mega-store. - In tests, use
AvenxMock.createMockBridge()andsandbox.waitForUpdate()so you assert after a single batched flush.
Teardown and long-lived SPAs
Section titled “Teardown and long-lived SPAs”Bridges registered for the app lifetime normally stay alive until the page unloads. If you create temporary bridges in a sandbox or a feature that unmounts:
- Drop references from your own registries so the instance can be garbage-collected.
- Clear timers, websocket listeners, or
documentlisteners that bridge methods attached. - Prefer resetting state (
logout(),clear()) over leaving stale tokens or cart lines in memory between user sessions.
TypeScript Guidelines
Section titled “TypeScript Guidelines”import { AvenxBridge } from 'avenx-core/runtime';
export interface AuthUser { name: string; role: 'visitor' | 'member' | 'admin';}
export default class AuthBridge extends AvenxBridge { isLoggedIn: boolean; token: string | null; user: AuthUser;
constructor() { super(); this.isLoggedIn = false; this.token = null; this.user = { name: 'Guest', role: 'visitor' }; }
login(user: AuthUser, token: string): void { this.isLoggedIn = true; this.token = token; this.user = user; }
logout(): void { this.isLoggedIn = false; this.token = null; this.user = { name: 'Guest', role: 'visitor' }; }}Declare field types on the class body, initialize them in the constructor, and type method parameters/returns. Templates still access the bridge as AuthBridge; TypeScript mainly improves bridge modules, IDE completion, and unit tests that import the class directly.