Skip to content

Event Bus

Every state change, route transition, component lifecycle event, and batch operation in Vio flows through a central event bus. This gives you full observability into your application — subscribe to any event, emit custom events, and inspect the event history.

Why an Event Bus?

Traditional frameworks scatter state changes across callbacks, watchers, and effects. Debugging means hunting through component trees and console logs. Vio takes a different approach: every mutation is an event, and every event is recorded.

This design serves two audiences:

  • Developers get a single place to observe, log, and react to everything happening in their app
  • AI agents get a structured, serializable stream of every operation — making the app fully inspectable and controllable via MCP devtools

Event Shape

Every event is a VioEvent object with three fields:

typescript
interface VioEvent {
  type: string                        // event identifier
  payload: Record<string, unknown>    // event-specific data
  timestamp: number                   // Date.now() when emitted
}

Built-in Event Types

Vio emits the following events automatically. You do not need to emit these yourself — they fire as part of normal framework operations.

Component Lifecycle

component:mount

Emitted when a component instance is mounted to the DOM.

typescript
app.on('component:mount', (event) => {
  // event.payload:
  // {
  //   name: 'Counter',
  //   id: 'Counter-1',
  //   state: { count: 0 }
  // }
})

component:update

Triggered when setState is called on a component. The underlying event on the bus is state:change (see below), while the onUpdate lifecycle hook fires on the component instance itself.

component:unmount

Emitted when a component is removed from the DOM.

typescript
app.on('component:unmount', (event) => {
  // event.payload:
  // {
  //   name: 'Counter',
  //   id: 'Counter-1'
  // }
})

State Changes

state:change

Emitted when a component's local state is updated via setState.

typescript
app.on('state:change', (event) => {
  // event.payload:
  // {
  //   component: 'Counter',
  //   id: 'Counter-1',
  //   prev: { count: 0 },
  //   next: { count: 1 }
  // }
})

store:change

Emitted when a global store action is dispatched.

typescript
app.on('store:change', (event) => {
  // event.payload:
  // {
  //   action: 'addTodo',
  //   payload: { text: 'Learn Vio' },
  //   prev: { todos: [] },
  //   next: { todos: [{ text: 'Learn Vio' }] }
  // }
})

Route Events

route:before

Emitted before a route change takes effect.

typescript
app.on('route:before', (event) => {
  // event.payload:
  // {
  //   from: '/',         // previous path, or null on first navigation
  //   to: '/dashboard'
  // }
})

route:change

Emitted after the route has been resolved.

typescript
app.on('route:change', (event) => {
  // event.payload:
  // {
  //   from: '/',
  //   to: '/users/42',
  //   params: { id: '42' }
  // }
})

route:after

Emitted after navigation is complete.

typescript
app.on('route:after', (event) => {
  // event.payload:
  // {
  //   path: '/users/42',
  //   params: { id: '42' }
  // }
})

Batch Operations

batch:start

Emitted when a batch operation begins.

typescript
app.on('batch:start', (event) => {
  // event.payload:
  // { operations: 3 }   // number of operations in the batch
})

batch:end

Emitted when a batch operation completes.

typescript
app.on('batch:end', (event) => {
  // event.payload:
  // { operations: 3 }
})

Subscribing to Events

Use app.on() to listen for events. It returns an unsubscribe function — call it to stop listening.

typescript
// Subscribe
const unsubscribe = app.on('state:change', (event) => {
  console.log(`${event.payload.component} state changed`)
})

// Later, unsubscribe
unsubscribe()

Listening to All Events

Use the wildcard * to receive every event that flows through the bus:

typescript
const unsubscribe = app.on('*', (event) => {
  console.log(`[${event.type}]`, event.payload)
})

This is particularly useful for debugging and logging.

Multiple Subscribers

You can attach as many handlers as you want to the same event type. They all fire in the order they were registered.

typescript
// Logger
app.on('store:change', (event) => {
  console.log('Store action:', event.payload.action)
})

// Persistence
app.on('store:change', (event) => {
  localStorage.setItem('state', JSON.stringify(event.payload.next))
})

// Analytics
app.on('store:change', (event) => {
  analytics.track('store_action', { action: event.payload.action })
})

Custom Events

Emit your own events using app.emit(). Custom events flow through the same bus and appear in the event history alongside built-in events.

typescript
// Emit a custom event
app.emit('analytics:pageview', { page: '/dashboard', referrer: '/' })
app.emit('notification:show', { message: 'Saved!', type: 'success' })
app.emit('feature:flag', { name: 'dark-mode', enabled: true })

Subscribe to custom events exactly like built-in ones:

typescript
app.on('notification:show', (event) => {
  showToast(event.payload.message as string, event.payload.type as string)
})

TIP

Use a consistent naming convention for custom events. The namespace:action pattern (e.g., analytics:pageview, notification:show) keeps things organized and avoids collisions with built-in events.

Event History

Vio keeps a rolling buffer of the last 100 events. Retrieve them with app.getEventHistory():

typescript
const history = app.getEventHistory()

// Returns an array of VioEvent objects, oldest first
// [
//   { type: 'component:mount', payload: {...}, timestamp: 1709001234567 },
//   { type: 'state:change', payload: {...}, timestamp: 1709001234589 },
//   ...
// ]

The history includes both built-in and custom events. When the buffer exceeds 100 entries, the oldest event is dropped.

Using History for Debugging

typescript
// Print all recent state changes
const stateChanges = app.getEventHistory()
  .filter(e => e.type === 'state:change')

stateChanges.forEach(e => {
  console.log(`[${new Date(e.timestamp).toISOString()}]`,
    `${e.payload.component}: `, e.payload.prev, '→', e.payload.next)
})

Using History for AI Agents

The event history is one of the key features that makes Vio AI-agent-friendly. An MCP tool can call getEventHistory() to understand what happened in the app — without needing to instrument anything manually.

typescript
// An MCP tool might do this:
const events = app.getEventHistory()
const lastNavigation = events.findLast(e => e.type === 'route:change')
const recentErrors = events.filter(e => e.type === 'error:occurred')

Practical Patterns

State Persistence

Automatically save and restore store state:

typescript
// Save on every change
app.on('store:change', (event) => {
  localStorage.setItem('vio-store', JSON.stringify(event.payload.next))
})

// Restore on app start (before mount)
const saved = localStorage.getItem('vio-store')
if (saved) {
  const restored = JSON.parse(saved)
  // Use restored data as initial store state
}

Route Analytics

Track page views:

typescript
app.on('route:after', (event) => {
  analytics.pageView({
    path: event.payload.path,
    params: event.payload.params,
    timestamp: event.timestamp
  })
})

Debug Logger

Log everything during development:

typescript
if (import.meta.env.DEV) {
  app.on('*', (event) => {
    const time = new Date(event.timestamp).toLocaleTimeString()
    console.log(`%c[${time}] ${event.type}`, 'color: #888', event.payload)
  })
}

Component Lifecycle Tracking

Monitor which components are alive:

typescript
const mounted = new Set<string>()

app.on('component:mount', (event) => {
  mounted.add(event.payload.id as string)
  console.log('Mounted components:', [...mounted])
})

app.on('component:unmount', (event) => {
  mounted.delete(event.payload.id as string)
  console.log('Mounted components:', [...mounted])
})

Event Reference

Event TypePayload FieldsWhen It Fires
component:mountname, id, stateComponent added to DOM
component:unmountname, idComponent removed from DOM
state:changecomponent, id, prev, nextsetState called on a component
store:changeaction, payload, prev, nextdispatch called on the store
route:beforefrom, toBefore route resolution
route:changefrom, to, paramsAfter route resolved
route:afterpath, paramsAfter navigation complete
batch:startoperationsBatch operation begins
batch:endoperationsBatch operation ends

Released under the MIT License.