Concepts

Event System

The event system is how plugins communicate with the devtools UI and with the application. It is built on , a type-safe event emitter/listener from . It is completely framework-agnostic.

EventClient Basics

Create a typed by extending the base class with your event map:

ts
import { EventClient } from '@tanstack/devtools-event-client'

type MyEvents = {
  'state-update': { count: number }
  'action': { type: string }
}

class MyEventClient extends EventClient<MyEvents> {
  constructor() {
    super({ pluginId: 'my-plugin' })
  }
}

export const myEventClient = new MyEventClient()
import { EventClient } from '@tanstack/devtools-event-client'

type MyEvents = {
  'state-update': { count: number }
  'action': { type: string }
}

class MyEventClient extends EventClient<MyEvents> {
  constructor() {
    super({ pluginId: 'my-plugin' })
  }
}

export const myEventClient = new MyEventClient()

The constructor accepts the following options:

OptionTypeDefaultDescription
Required. Identifies this plugin in the event system.
Enable debug logging to the console.
Whether the client connects to the bus at all.
Interval (ms) between connection retry attempts.

Event Maps and Type Safety

The generic type maps event names to payload types. Keys are event suffixes only — the is prepended automatically by when emitting and listening:

ts
type MyEvents = {
  'state-update': { count: number }
  'action': { type: string }
}
type MyEvents = {
  'state-update': { count: number }
  'action': { type: string }
}

TypeScript enforces correct event names and payload shapes at compile time. You get autocomplete on event names and type errors if the payload does not match the declared shape.

Emitting Events

Call to dispatch an event. You pass only the suffix (the part after the colon). The is prepended automatically:

ts
// If pluginId is 'my-plugin' and event map has 'state-update'
myEventClient.emit('state-update', { count: 42 })
// Dispatches event named 'my-plugin:state-update'
// If pluginId is 'my-plugin' and event map has 'state-update'
myEventClient.emit('state-update', { count: 42 })
// Dispatches event named 'my-plugin:state-update'

If the client is not yet connected to the bus, the event is queued and flushed once the connection succeeds (see Connection Lifecycle below).

Listening to Events

There are three methods for subscribing to events. Each returns a cleanup function you call to unsubscribe.

Listen to a specific event from this plugin. Like , you pass only the suffix:

ts
const cleanup = myEventClient.on('state-update', (event) => {
  console.log(event.payload.count) // typed as { count: number }
})

// Later: stop listening
cleanup()
const cleanup = myEventClient.on('state-update', (event) => {
  console.log(event.payload.count) // typed as { count: number }
})

// Later: stop listening
cleanup()

The callback receives the full event object:

ts
{
  type: 'my-plugin:state-update', // fully qualified event name
  payload: { count: number },     // typed payload
  pluginId: 'my-plugin'           // originating plugin
}
{
  type: 'my-plugin:state-update', // fully qualified event name
  payload: { count: number },     // typed payload
  pluginId: 'my-plugin'           // originating plugin
}

Listen to all events from all plugins. Useful for logging, debugging, or building cross-plugin features:

ts
const cleanup = myEventClient.onAll((event) => {
  console.log(event.type, event.payload)
})
const cleanup = myEventClient.onAll((event) => {
  console.log(event.type, event.payload)
})

Listen to all events from this plugin only (filtered by ):

ts
const cleanup = myEventClient.onAllPluginEvents((event) => {
  // Only fires for events where event.pluginId === 'my-plugin'
  console.log(event.type, event.payload)
})
const cleanup = myEventClient.onAllPluginEvents((event) => {
  // Only fires for events where event.pluginId === 'my-plugin'
  console.log(event.type, event.payload)
})

Connection Lifecycle

The manages its connection to the event bus automatically:

mermaid
stateDiagram-v2
    [*] --> Queueing: First emit() call
    Queueing --> Connecting: Dispatches tanstack-connect
    Connecting --> Retrying: No response
    Retrying --> Connecting: Every 300ms (up to 5×)
    Connecting --> Connected: tanstack-connect-success
    Connected --> Connected: Emit events directly
    Queueing --> Connected: Flush queued events
    Retrying --> Failed: 5 retries exhausted
    Failed --> [*]: Subsequent emits dropped
  1. Queueing — When you call before the client is connected, events are queued in memory.
  2. Connection — On the first , the client dispatches a event and starts a retry loop.
  3. Retries — The client retries every (default: 300ms) up to a maximum of 5 attempts.
  4. Flush — Once a event is received, all queued events are flushed to the bus in order.
  5. Failure — If all 5 retries are exhausted without a successful connection, the client stops retrying. Subsequent calls are silently dropped (they will not be queued).

The Option

When is set to , the EventClient is effectively inert — is a no-op and returns a no-op cleanup function. This is useful for conditionally disabling devtools instrumentation (e.g., in production).

Server Event Bus

When is set in the component's prop, the connects to the started by the Vite plugin (default port 4206). This enables server-side features like console piping and the plugin marketplace.

tsx
<TanStackDevtools
  eventBusConfig={{
    connectToServerBus: true,
    debug: false,
    port: 4206, // default
  }}
/>
<TanStackDevtools
  eventBusConfig={{
    connectToServerBus: true,
    debug: false,
    port: 4206, // default
  }}
/>

Without the Vite plugin running, the still works for same-page communication between your application code and the devtools panel. The server bus is only needed for features that bridge the browser and the dev server.

Debugging

Set in the constructor or in to enable verbose console logging. Debug logs are prefixed with for plugin events and for the client bus.

ts
const myEventClient = new MyEventClient()
// In the constructor: super({ pluginId: 'my-plugin', debug: true })
const myEventClient = new MyEventClient()
// In the constructor: super({ pluginId: 'my-plugin', debug: true })

Example output:

plaintext
🌴 [tanstack-devtools:client-bus] Initializing client event bus
🌴 [tanstack-devtools:my-plugin] Registered event to bus my-plugin:state-update
🌴 [tanstack-devtools:my-plugin] Emitting event my-plugin:state-update
🌴 [tanstack-devtools:client-bus] Initializing client event bus
🌴 [tanstack-devtools:my-plugin] Registered event to bus my-plugin:state-update
🌴 [tanstack-devtools:my-plugin] Emitting event my-plugin:state-update

This is helpful when diagnosing issues with event delivery, connection timing, or verifying that your event map is wired up correctly.