Skip to content

Shared State & Bridges


Bridges provide an elegant, lightweight solution for sharing state and business logic across multiple components or pages without prop-drilling.

Generate a new bridge using the CLI tool:

Terminal window
npx avenx g bridge auth

This creates a file in src/global/auth.bridge.js. The generated bridge uses the class-based pattern and extends the AvenxBridge base class:

src/global/auth.bridge.js
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 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.

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>

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:

src/global/auth.bridge.js
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.

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.

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.

src/global/theme.bridge.js
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.

src/global/auth.bridge.js
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>
src/global/cart.bridge.js
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>
  1. The compiler discovers *.bridge.js files and registers each bridge on the app (typically as NameBridge).
  2. A component template or action reads SomeBridge.property — the runtime tracks that dependency.
  3. Another component (or the bridge itself) mutates SomeBridge.property.
  4. 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());

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].

  • 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() and sandbox.waitForUpdate() so you assert after a single batched flush.

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 document listeners that bridge methods attached.
  • Prefer resetting state (logout(), clear()) over leaving stale tokens or cart lines in memory between user sessions.
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.