Skip to main content

devtools-plugin-panel

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.

소스 정보

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

설치 방법

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

소스 파일 검토

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

파일 탐색기
2 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
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. ```ts // Source: packages/devtools/src/context/devtools-context.tsx interface TanStackDevtoolsPlugin { id?: string name: string | ((el: HTMLHeadingElement, theme: 'dark' | 'light') => void) render: (el: HTMLDivElement, theme: 'dark' | 'light') => void destroy?: (pluginId: string) => void defaultOpen?: 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 ```ts // src/event-client.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' }) } } 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 ```tsx /** @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 ```ts // src/core.ts import { constructCoreClass } from '@tanstack/devtools-utils/solid/class' export const [StoreInspectorCore, NoOpStoreInspectorCore] = constructCoreClass( () => import('./panel'), ) ``` ```tsx // src/react.tsx import { createReactPanel } from '@tanstack/devtools-utils/react' import { StoreInspectorCore } from './core' export const [StoreInspectorPanel, NoOpStoreInspectorPanel] = createReactPanel(StoreInspectorCore) ``` ```tsx // src/react-plugin.tsx import { 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 ```tsx 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) ```tsx 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 1. **Initialization** -- `TanStackDevtoolsCore` receives `plugins` array. Each plugin gets an `id` (explicit or generated). 2. **DOM containers created** -- Core creates `<div id="plugin-container-{id}">` and `<h3 id="plugin-title-container-{id}">` per plugin. 3. **Activation** -- On tab click or `defaultOpen`, `plugin.render(container, theme)` called. 4. **Framework portaling** -- React uses `createPortal`, Solid uses `<Portal>`, Vue uses `<Teleport>`. 5. **Theme change** -- `render` called again with new theme value. 6. **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: ```ts useEffect(() => { client.on('state', cb) }, []) ``` Correct: ```ts useEffect(() => { const cleanup = client.on('state', cb) return cleanup }, []) ``` In Solid, use `onCleanup()`: ```ts 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: ```ts function ComponentA() { useEffect(() => { const c = client.on('state', cb1) return c }, []) } function ComponentB() { useEffect(() => { const c = client.on('state', cb2) return c }, []) } ``` Correct: ```ts 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: ```ts client.emit('state-changed', { storeName: 'main', version: '1.0', state }) client.emit('action-dispatched', { storeName: 'main', version: '1.0', action }) ``` Correct: ```ts 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: ```tsx function MyPanel() { return <div style={{ color: 'white' }}>Always white text</div> } ``` Correct: ```tsx 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: ```ts render: (el) => { const div = document.createElement('div') div.textContent = 'Hello' el.appendChild(div) } ``` Correct: ```tsx import { createReactPlugin } from '@tanstack/devtools-utils/react' const [Plugin, NoOpPlugin] = createReactPlugin({ name: 'My Plugin', Component: ({ theme }) => <div>Hello</div>, })
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기