Skill v1.0.1
currentAutomated scan96/100+3 new
version: "1.0.1" name: devtools-plugin-panel description: > Build devtools panel components that display emitted event data. Listen via EventClient.on(), handle theme (light/dark), use @tanstack/devtools-ui components. Plugin registration (name, render, id, defaultOpen), lifecycle (mount, activate, destroy), max 3 active plugins. Two paths: Solid.js core with devtools-ui for multi-framework support, or framework-specific panels. type: core library: tanstack-devtools library_version: '0.10.12' requires:
- devtools-event-client
sources:
- 'TanStack/devtools:docs/building-custom-plugins.md'
- 'TanStack/devtools:docs/plugin-lifecycle.md'
- 'TanStack/devtools:docs/plugin-configuration.md'
- 'TanStack/devtools:packages/devtools/src/context/devtools-context.tsx'
TanStackDevtoolsPlugin Interface
The low-level contract every plugin implements. Framework adapters wrap this automatically.
// Source: packages/devtools/src/context/devtools-context.tsxinterface TanStackDevtoolsPlugin {id?: stringname: string | ((el: HTMLHeadingElement, theme: 'dark' | 'light') => void)render: (el: HTMLDivElement, theme: 'dark' | 'light') => voiddestroy?: (pluginId: string) => voiddefaultOpen?: boolean}
- `name` (required) -- String tab title, or function receiving
(el, theme)for custom rendering. - `render` (required) -- Called on activation with a
<div>container and theme. Called again on theme change. - `id` (optional) -- Stable identifier. If omitted:
name.toLowerCase().replace(' ', '-')-{index}. Explicit ids persist selection across reloads. - `defaultOpen` (optional) -- Opens panel on first load when no saved state. Max 3 open. Does not override saved preferences.
- `destroy` (optional) -- Called on deactivation or unmount. Framework adapters handle cleanup automatically.
Two Development Paths
Path 1: Solid.js Core + Framework Adapters (Multi-Framework)
Build the panel in Solid.js using @tanstack/devtools-ui components. Use constructCoreClass for lazy loading, then createReactPanel/createSolidPanel to wrap for each framework. The devtools core is Solid, so Solid panels run natively.
Path 2: Framework-Specific Panel (Single Framework)
Build directly in your framework and use createReactPlugin/createVuePlugin/createSolidPlugin/createPreactPlugin from @tanstack/devtools-utils.
Path 1: Solid.js Core Panel
Step 1: Define Event Map and Create EventClient
// src/event-client.tsimport { 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' })}}export const storeInspector = new StoreInspectorClient()
Event names are suffixes only. The pluginId is prepended automatically: 'store-inspector:state-changed'.
Step 2: Build the Solid.js Panel Component
/** @jsxImportSource solid-js */import { createSignal, onCleanup, For } from 'solid-js'import {MainPanel,Header,HeaderLogo,Section,SectionTitle,JsonTree,Button,Tag,createTheme,} from '@tanstack/devtools-ui'import { storeInspector } from './event-client'export default function StoreInspectorPanel() {const { theme } = createTheme()const [state, setState] = createSignal<Record<string, unknown>>({})const [actions, setActions] = createSignal<Array<{ action: string; payload: unknown }>>([])const cleanupState = storeInspector.on('state-changed', (e) => {setState((prev) => ({ ...prev, [e.payload.storeName]: e.payload.state }))})const cleanupActions = storeInspector.on('action-dispatched', (e) => {setActions((prev) => [...prev,{ action: e.payload.action, payload: e.payload.payload },])})onCleanup(() => {cleanupState()cleanupActions()})return (<MainPanel><Header><HeaderLogo flavor={{ light: '#1a1a2e', dark: '#e0e0e0' }}>Store Inspector</HeaderLogo></Header><Section><SectionTitle>Current State</SectionTitle><JsonTree value={state()} copyable defaultExpansionDepth={2} /></Section><Section><SectionTitle>Action Log<Tag color="purple" label="Actions" count={actions().length} /></SectionTitle><For each={actions()}>{(a) => (<div><strong>{a.action}</strong><JsonTree value={a.payload} copyable defaultExpansionDepth={1} /></div>)}</For><Button variant="danger" onClick={() => setActions([])}>Clear Log</Button></Section></MainPanel>)}
Step 3: Create Core Class and Framework Adapters
// src/core.tsimport { constructCoreClass } from '@tanstack/devtools-utils/solid/class'export const [StoreInspectorCore, NoOpStoreInspectorCore] = constructCoreClass(() => import('./panel'),)
// src/react.tsximport { createReactPanel } from '@tanstack/devtools-utils/react'import { StoreInspectorCore } from './core'export const [StoreInspectorPanel, NoOpStoreInspectorPanel] =createReactPanel(StoreInspectorCore)
// src/react-plugin.tsximport { createReactPlugin } from '@tanstack/devtools-utils/react'import { StoreInspectorPanel } from './react'export const [StoreInspectorPlugin, NoOpStoreInspectorPlugin] =createReactPlugin({name: 'Store Inspector',id: 'store-inspector',defaultOpen: true,Component: StoreInspectorPanel,})
Step 4: Register
import { TanStackDevtools } from '@tanstack/react-devtools'import { StoreInspectorPlugin } from 'your-package/react-plugin'function App() {return (<><YourApp /><TanStackDevtools plugins={[StoreInspectorPlugin()]} /></>)}
Path 2: Framework-Specific Panel (React Example)
import { useState, useEffect } from 'react'import { EventClient } from '@tanstack/devtools-event-client'import { createReactPlugin } from '@tanstack/devtools-utils/react'type MyEvents = {'data-update': { items: Array<{ id: string; value: number }> }}class MyPluginClient extends EventClient<MyEvents> {constructor() {super({ pluginId: 'my-plugin' })}}export const myPlugin = new MyPluginClient()function MyPluginPanel({ theme }: { theme?: 'light' | 'dark' }) {const [items, setItems] = useState<Array<{ id: string; value: number }>>([])useEffect(() => {const cleanup = myPlugin.on('data-update', (e) => {setItems(e.payload.items)})return cleanup}, [])return (<div style={{ color: theme === 'dark' ? '#fff' : '#000' }}><h3>My Plugin</h3><ul>{items.map((item) => (<li key={item.id}>{item.id}: {item.value}</li>))}</ul></div>)}export const [MyPlugin, NoOpMyPlugin] = createReactPlugin({name: 'My Plugin',id: 'my-plugin',defaultOpen: false,Component: MyPluginPanel,})
Plugin Lifecycle Sequence
- Initialization --
TanStackDevtoolsCorereceivespluginsarray. Each plugin gets anid(explicit or generated). - DOM containers created -- Core creates
<div id="plugin-container-{id}">and<h3 id="plugin-title-container-{id}">per plugin. - Activation -- On tab click or
defaultOpen,plugin.render(container, theme)called. - Framework portaling -- React uses
createPortal, Solid uses<Portal>, Vue uses<Teleport>. - Theme change --
rendercalled again with new theme value. - Deactivation/Unmount --
destroy(pluginId)called if provided. Framework adapters handle cleanup.
Active plugin selection persisted in localStorage under key tanstack_devtools_state.
Common Mistakes
CRITICAL: Not Cleaning Up Event Listeners
Each on() returns a cleanup function. Forgetting it causes memory leaks and duplicate handlers.
Wrong:
useEffect(() => {client.on('state', cb)}, [])
Correct:
useEffect(() => {const cleanup = client.on('state', cb)return cleanup}, [])
In Solid, use onCleanup():
const cleanup = storeInspector.on('state-changed', handler)onCleanup(cleanup)
Source: docs/building-custom-plugins.md
HIGH: Oversubscribing to Events in Multiple Components
Do not call on() in multiple components for the same event. Subscribe once in a shared store/hook.
Wrong:
function ComponentA() {useEffect(() => {const c = client.on('state', cb1)return c}, [])}function ComponentB() {useEffect(() => {const c = client.on('state', cb2)return c}, [])}
Correct:
function useStoreState() {const [state, setState] = useState(null)useEffect(() => {const cleanup = client.on('state', (e) => setState(e.payload))return cleanup}, [])return state}
Source: maintainer interview
MEDIUM: Hardcoding Repeated Event Payload Fields
When emitting events that share common fields, create a shared base object.
Wrong:
client.emit('state-changed', { storeName: 'main', version: '1.0', state })client.emit('action-dispatched', { storeName: 'main', version: '1.0', action })
Correct:
const base = { storeName: 'main', version: '1.0' }client.emit('state-changed', { ...base, state })client.emit('action-dispatched', { ...base, action })
Source: maintainer interview
MEDIUM: Ignoring Theme Prop in Panel Component
Panels must adapt styling to theme. Factory-created plugins receive props.theme.
Wrong:
function MyPanel() {return <div style={{ color: 'white' }}>Always white text</div>}
Correct:
function MyPanel({ theme }: { theme?: 'light' | 'dark' }) {return (<div style={{ color: theme === 'dark' ? '#e0e0e0' : '#1a1a1a' }}>Theme-aware text</div>)}
In Solid panels using devtools-ui, use createTheme() instead of prop drilling.
Source: docs/plugin-lifecycle.md
MEDIUM: Not Knowing Max 3 Active Plugins Limit
MAX_ACTIVE_PLUGINS = 3 (in packages/devtools/src/utils/constants.ts). If more than 3 set defaultOpen: true, only the first 3 open. Activating a 4th deactivates the earliest. Single-plugin exception: if only 1 plugin is registered, it opens automatically.
Source: packages/devtools/src/utils/get-default-active-plugins.ts
MEDIUM: Using Raw DOM Manipulation Instead of Framework Portals
Framework adapters handle portaling. Do not manually manipulate DOM.
Wrong:
render: (el) => {const div = document.createElement('div')div.textContent = 'Hello'el.appendChild(div)}
Correct:
import { createReactPlugin } from '@tanstack/devtools-utils/react'const [Plugin, NoOpPlugin] = createReactPlugin({name: 'My Plugin',Component: ({ theme }) => <div>Hello</div>,})
Source: docs/plugin-lifecycle.md
MEDIUM: Not Keeping Devtools Packages at Latest Versions
All @tanstack/devtools-* packages should be on compatible versions. For external plugins, pin to compatible ranges.
Source: maintainer interview