Skip to main content

devtools-event-client

Create typed EventClient for a library. Define event maps with typed payloads, pluginId auto-prepend namespacing, emit()/on()/onAll()/onAllPluginEvents() API. Connection lifecycle (5 retries, 300ms), event queuing, enabled/disabled state, SSR fallbacks, singleton pattern. Unique pluginId requirement to avoid event collisions.

소스 정보

저장소
TanStack/devtools
최근 소스 활동
2026년 6월 24일 11:13
감지된 SKILL.md 언어
영어
스타
503
포크
101

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
devtools-event-client
description
Create typed EventClient for a library. Define event maps with typed payloads, pluginId auto-prepend namespacing, emit()/on()/onAll()/onAllPluginEvents() API. Connection lifecycle (5 retries, 300ms), event queuing, enabled/disabled state, SSR fallbacks, singleton pattern. Unique pluginId requirement to avoid event collisions.
type
core
library
@tanstack/devtools-event-client
library_version
0.10.12
sources
["packages/event-bus-client/src/plugin.ts","docs/event-system.md","docs/building-custom-plugins.md"]
# devtools-event-client Typed event emitter/listener that connects application code to TanStack Devtools panels. Framework-agnostic. Works in React, Vue, Solid, Preact, and vanilla JS. ## Setup Install the package: ```bash npm i @tanstack/devtools-event-client ``` The package has two entry points — the root export (the real client in development, a no-op tree-shaken out of production), and the `/production` subpath (always the real client, for libraries that want devtools events in production): ```ts import { EventClient } from '@tanstack/devtools-event-client' ``` ### Constructor Options | Option | Type | Required | Default | Description | | ------------------ | --------- | -------- | ------- | ------------------------------------------------------------------------------------- | | `pluginId` | `string` | Yes | -- | Identifies this plugin in the event system. Must be unique across all plugins. | | `debug` | `boolean` | No | `false` | Enable verbose console logging prefixed with `[tanstack-devtools:{pluginId}-plugin]`. | | `enabled` | `boolean` | No | `true` | When `false`, `emit()` is a no-op and `on()` returns a no-op cleanup function. | | `reconnectEveryMs` | `number` | No | `300` | Interval in ms between connection retry attempts (max 5 retries). | ## Core Patterns ### 1. Define an Event Map and Create a Singleton Client Define a TypeScript type mapping event suffixes to payload types. Extend `EventClient` and export a single instance at module level. ```ts import { EventClient } from '@tanstack/devtools-event-client' type StoreEvents = { 'state-changed': { storeName: string; state: unknown; timestamp: number } 'action-dispatched': { storeName: string; action: string; payload: unknown } reset: void } class StoreInspectorClient extends EventClient<StoreEvents> { constructor() { super({ pluginId: 'store-inspector' }) } } // Module-level singleton -- one instance per plugin export const storeInspector = new StoreInspectorClient() ``` Event map keys are suffixes only. The `pluginId` is prepended automatically. With `pluginId: 'store-inspector'` and key `'state-changed'`, the fully qualified event on the bus is `'store-inspector:state-changed'`. ### 2. Emit Events Call `emit(suffix, payload)` from library code. Pass only the suffix. ```ts function dispatch(action: string, payload: unknown) { state = reducer(state, action, payload) storeInspector.emit('state-changed', { storeName: 'main', state, timestamp: Date.now(), }) storeInspector.emit('action-dispatched', { storeName: 'main', action, payload, }) } ``` If the bus is not connected yet, events are queued in memory and flushed once the connection succeeds. If the connection fails after 5 retries (1.5s at default settings), the client gives up and subsequent `emit()` calls are silently dropped. Connection to the bus is initiated lazily on the first `emit()` call, not on construction or `on()`. ### 3. Listen to Events All listener methods return a cleanup function. **`on(suffix, callback)`** -- listen to a specific event from this plugin: ```ts const cleanup = storeInspector.on('state-changed', (event) => { // event.type === 'store-inspector:state-changed' // event.payload === { storeName: string; state: unknown; timestamp: number } // event.pluginId === 'store-inspector' console.log(event.payload.state) }) // Stop listening cleanup() ``` **`on(suffix, callback, { withEventTarget: true })`** -- also register on an internal EventTarget so events emitted and listened to on the same client instance are delivered immediately without going through the global bus: ```ts const cleanup = storeInspector.on( 'state-changed', (event) => { console.log(event.payload.state) }, { withEventTarget: true }, ) ``` **`onAll(callback)`** -- listen to all events from all plugins: ```ts const cleanup = storeInspector.onAll((event) => { console.log(event.type, event.payload) }) ``` **`onAllPluginEvents(callback)`** -- listen to all events from this plugin only (filtered by `pluginId`): ```ts const cleanup = storeInspector.onAllPluginEvents((event) => { // Only fires when event.pluginId === 'store-inspector' console.log(event.type, event.payload) }) ``` ### 4. Connection Lifecycle and Disabling The connection lifecycle is: 1. First `emit()` dispatches `tanstack-connect` and starts a retry loop. 2. Retries every `reconnectEveryMs` (default 300ms), up to 5 attempts. 3. On `tanstack-connect-success`, queued events are flushed in order. 4. After 5 failed retries, `failedToConnect` is set permanently. All subsequent `emit()` calls are silently dropped (not queued). To disable the client entirely (e.g., in production): ```ts class StoreInspectorClient extends EventClient<StoreEvents> { constructor() { super({ pluginId: 'store-inspector', enabled: process.env.NODE_ENV !== 'production', }) } } ``` When `enabled` is `false`, `emit()` is a no-op and `on()`/`onAll()`/`onAllPluginEvents()` return no-op cleanup functions. ## Common Mistakes ### 1. Including pluginId prefix in event names (CRITICAL) `EventClient` auto-prepends the `pluginId` to all event names. Including the prefix manually produces a double-prefixed event name that nothing will match. Wrong: ```ts storeInspector.emit('store-inspector:state-changed', data) // Dispatches 'store-inspector:store-inspector:state-changed' ``` Correct: ```ts storeInspector.emit('state-changed', data) // Dispatches 'store-inspector:state-changed' ``` This applies to `on()` as well. Pass only the suffix. ### 2. Creating multiple EventClient instances per plugin (CRITICAL) Each `EventClient` instance manages its own connection, event queue, and listeners independently. Creating multiple instances for the same plugin causes duplicate handlers, multiple connection attempts, and unpredictable event delivery. Wrong: ```tsx function MyComponent() { // New instance on every render const client = new StoreInspectorClient() client.emit('state-changed', data) } ``` Correct: ```ts // store-inspector-client.ts export const storeInspector = new StoreInspectorClient() // MyComponent.tsx import { storeInspector } from './store-inspector-client' function MyComponent() { storeInspector.emit('state-changed', data) } ``` ### 3. Non-unique pluginId causing event collisions (CRITICAL) Two plugins with the same `pluginId` share an event namespace. Events emitted by one are received by listeners on the other. Choose a unique, descriptive `pluginId` (e.g., `'my-org-store-inspector'` rather than `'store'`). ### 4. Not realizing events drop after 5 failed retries (HIGH) After 5 retries (1.5s at default `reconnectEveryMs: 300`), `failedToConnect` is set permanently. Subsequent `emit()` calls are silently dropped -- they are not queued and will never be delivered, even if the bus becomes available later. If you need events to survive longer startup delays, increase `reconnectEveryMs`: ```ts super({ pluginId: 'store-inspector', reconnectEveryMs: 1000 }) // 5 retries * 1000ms = 5s window ``` There is no way to increase the retry count (hardcoded to 5). ### 5. Expecting connection on construction or on() (HIGH) The connection to the event bus is initiated lazily on the first `emit()` call. Calling `on()` alone does not trigger a connection. If your panel calls `on()` but the library side never calls `emit()`, the client never connects to the bus. This means if you only listen (no emitting), the `on()` handler still works for events dispatched directly on the global event target, but the connection handshake (`tanstack-connect` / `tanstack-connect-success`) never runs. ### 6. Using non-serializable payloads (HIGH) When the server event bus is enabled, events are serialized via JSON for transport over WebSocket/SSE/BroadcastChannel. Payloads containing functions, DOM nodes, class instances, `Map`/`Set`, or circular references will fail silently or lose data. Wrong: ```ts storeInspector.emit('state-changed', { storeName: 'main', state, callback: () => {}, // Function -- not serializable element: document.body, // DOM node -- not serializable }) ``` Correct: ```ts storeInspector.emit('state-changed', { storeName: 'main', state: JSON.parse(JSON.stringify(state)), // Ensure serializable timestamp: Date.now(), }) ``` ### 7. Forgetting the root export no-ops in production (HIGH) The **root import** of `@tanstack/devtools-event-client` resolves to a no-op when `process.env.NODE_ENV !== 'development'`, and the real client is tree-shaken out of production bundles. This is the default and what you want for most libraries — your `emit()` calls cost nothing in production. "Outside development" includes when `NODE_ENV` is unset — common in plain Node scripts, some SSR dev servers, and test runners — so the root import resolves to the no-op there too. Set `NODE_ENV=development`, or use the `/production` subpath, to get the real client in those contexts. ```ts // dev: real client — production: no-op, removed from the bundle import { EventClient } from '@tanstack/devtools-event-client' ``` If you are publishing an open-source library and deliberately want devtools events to keep working in production, import from the `/production` subpath, which always ships the real client: ```ts import { EventClient } from '@tanstack/devtools-event-client/production' ``` The `enabled` constructor option still works for fine-grained runtime control, but you no longer need to guard `emit()` calls manually for bundle size — the root export handles that for you. ## See Also - `devtools-instrumentation` -- after creating a client, instrument library code with strategic emissions - `devtools-plugin-panel` -- the client emits events, the panel listens using the same event map - `devtools-bidirectional` -- two-way communication between panel and application using the same EventClient
GitHub에서 보기