fix(devtools-utils): align Svelte factory contracts (#504) · TanStack/devtools@f1584c7 · GitHub
Skip to content

Commit f1584c7

Browse files
authored
fix(devtools-utils): align Svelte factory contracts (#504)
* fix(devtools-utils): align Svelte factory contracts Closes #503 * fix(svelte-devtools): replace mounted plugin components * fix(svelte-devtools): clean up replaced plugin panels * fix(devtools-utils): own Svelte panel lifecycle * fix(svelte-devtools): preserve plugin state on updates
1 parent 4ecd46e commit f1584c7

23 files changed

Lines changed: 391 additions & 146 deletions
Lines changed: 6 additions & 0 deletions

‎docs/architecture.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,15 +190,17 @@ This client is a singleton (`devtoolsEventClient`) used by both the core shell a
190190
Each framework adapter is a thin wrapper that bridges its framework's component model to the core Solid.js shell. The pattern is the same across all adapters:
191191

192192
1. **Creates a `TanStackDevtoolsCore` instance** with the user's plugins and config.
193-
2. **Mounts it to a DOM element** using the framework's lifecycle hooks (`useEffect` in React, `onMounted` in Vue, `onMount` in Solid).
194-
3. **Converts framework-specific plugin definitions** into the core's DOM-based `render(el, theme)` interface. Each adapter defines its own plugin type (e.g. `TanStackDevtoolsReactPlugin`) that accepts framework-native components, then wraps them in a `render` callback that the core calls with a target DOM element and the current theme.
195-
4. **Uses the framework's portal/teleport mechanism** to render plugin components into the core's DOM containers:
193+
2. **Mounts it to a DOM element** using the framework's lifecycle APIs, such as `useEffect` in React, `onMounted` in Vue, `$effect` in Svelte, and `onMount` in Solid.
194+
3. **Converts framework-specific plugin definitions** into the core's DOM-based `render(el, props)` interface. Each adapter defines its own plugin type (e.g. `TanStackDevtoolsReactPlugin`) that accepts framework-native components, then wraps them in a `render` callback that the core calls with a target DOM element and `{ theme, devtoolsOpen }`.
195+
4. **Uses the framework's rendering API** to render plugin components into the core's DOM containers:
196196
- **React** -- `createPortal()` from `react-dom`
197197
- **Vue** -- `<Teleport :to="'#' + plugin.id" />`
198198
- **Solid** -- `<Portal mount={el} />`
199199
- **Preact** -- Same portal pattern as React
200+
- **Svelte** -- `mount(component, { target: el, props })`
201+
- **Angular** -- `createComponent()` with the container's environment injector
200202

201-
The key insight: the core shell is always Solid.js, but your plugins run in **your** framework. A React plugin is a real React component rendered by React's `createPortal` into a DOM element that the Solid.js shell created. A Vue plugin is a real Vue component rendered by Vue's `<Teleport>`. The adapters bridge this gap so you never need to think about Solid.js unless you want to.
203+
The key insight: the core shell is always Solid.js, but your plugins run in **your** framework. Each adapter receives the same plugin props and renders a native framework component into a DOM element created by the Solid.js shell. The adapters bridge this gap so you never need to think about Solid.js unless you want to.
202204

203205
### What an adapter does NOT do
204206

‎docs/devtools-utils.md‎

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const [MyPlugin, NoOpPlugin] = createReactPlugin({
7777

7878
The returned tuple contains two factory functions:
7979

80-
- **`Plugin()`** -- returns a plugin object with `name`, `id`, `defaultOpen`, and a `render` function that renders your `Component` with the current theme.
80+
- **`Plugin()`** -- returns a plugin object with `name`, `id`, `defaultOpen`, and a `render` function that renders your `Component` with the current plugin props.
8181
- **`NoOpPlugin()`** -- returns a plugin object with the same metadata but a `render` function that renders an empty fragment. Use this for production builds where you want to strip devtools out.
8282

8383
A common pattern for tree-shaking:
@@ -90,15 +90,15 @@ const ActivePlugin = process.env.NODE_ENV === 'development' ? MyPlugin : NoOpPlu
9090

9191
### createReactPanel
9292

93-
For library authors shipping a class-based devtools core that exposes `mount(el, theme)` and `unmount()` methods. This factory wraps that class in a React component that handles mounting into a `div`, passing the theme, and cleaning up on unmount.
93+
For library authors shipping a class-based devtools core that exposes `mount(el, props)` and `unmount()` methods. This factory wraps that class in a React component that handles mounting into a `div`, passing the complete plugin props, and cleaning up on unmount.
9494

9595
**Signature:**
9696

9797
```ts
9898
function createReactPanel<
99-
TComponentProps extends DevtoolsPanelProps | undefined,
99+
TComponentProps extends DevtoolsPanelProps,
100100
TCoreDevtoolsClass extends {
101-
mount: (el: HTMLElement, theme: 'light' | 'dark') => void
101+
mount: (el: HTMLElement, props: DevtoolsPanelProps) => void
102102
unmount: () => void
103103
},
104104
>(CoreClass: new () => TCoreDevtoolsClass): readonly [Panel, NoOpPanel]
@@ -107,10 +107,13 @@ function createReactPanel<
107107
**Usage:**
108108

109109
```tsx
110-
import { createReactPanel } from '@tanstack/devtools-utils/react'
110+
import {
111+
createReactPanel,
112+
type DevtoolsPanelProps,
113+
} from '@tanstack/devtools-utils/react'
111114

112115
class MyDevtoolsCore {
113-
mount(el: HTMLElement, theme: 'light' | 'dark') {
116+
mount(el: HTMLElement, props: DevtoolsPanelProps) {
114117
// render your devtools UI into el
115118
}
116119
unmount() {
@@ -129,9 +132,9 @@ const [MyPlugin, NoOpPlugin] = createReactPlugin({
129132

130133
The returned `Panel` component:
131134
- Creates a `div` with `height: 100%` and stores a ref to it.
132-
- Instantiates `CoreClass` on mount and calls `core.mount(el, theme)`.
135+
- Instantiates `CoreClass` on mount and calls `core.mount(el, props)`.
133136
- Calls `core.unmount()` on cleanup.
134-
- Re-mounts when the `theme` prop changes.
137+
- Re-mounts when the plugin props change.
135138

136139
`NoOpPanel` renders an empty fragment and does nothing.
137140

@@ -262,14 +265,18 @@ The panel component accepts `theme` and `devtoolsProps` as props. It mounts the
262265

263266
### createSveltePlugin
264267

265-
The Svelte factory takes a `name` string and a Svelte component as separate arguments, similar to the Vue API.
268+
The Svelte factory takes the plugin metadata and Svelte component in a configuration object.
266269

267270
**Signature:**
268271

269272
```ts
270273
function createSveltePlugin<TComponentProps extends Record<string, any>>(
271-
name: string,
272-
component: Component<TComponentProps>,
274+
config: {
275+
name: string
276+
id?: string
277+
defaultOpen?: boolean
278+
Component: Component<TComponentProps>
279+
},
273280
): readonly [Plugin, NoOpPlugin]
274281
```
275282

@@ -279,13 +286,18 @@ function createSveltePlugin<TComponentProps extends Record<string, any>>(
279286
import { createSveltePlugin } from '@tanstack/devtools-utils/svelte'
280287
import MyStorePanel from './MyStorePanel.svelte'
281288
282-
const [MyPlugin, NoOpPlugin] = createSveltePlugin('My Store', MyStorePanel)
289+
const [MyPlugin, NoOpPlugin] = createSveltePlugin({
290+
Component: MyStorePanel,
291+
name: 'My Store',
292+
id: 'my-store',
293+
defaultOpen: true,
294+
})
283295
```
284296

285297
The returned functions:
286298

287-
- **`Plugin(props?)`** -- returns `{ name, component, props }` where `component` is your Svelte component.
288-
- **`NoOpPlugin(props?)`** -- returns `{ name, component: NoOpComponent, props }` where the component renders nothing visible.
299+
- **`Plugin(props?)`** -- returns the plugin metadata, component, and forwarded props.
300+
- **`NoOpPlugin(props?)`** -- preserves the plugin metadata and props but replaces the component with one that renders nothing visible.
289301

290302
Both accept an optional `props` object that gets forwarded to the component on mount.
291303

@@ -299,7 +311,7 @@ import { createSveltePanel } from '@tanstack/devtools-utils/svelte'
299311
const [MyPanel, NoOpPanel] = createSveltePanel(MyDevtoolsCore)
300312
```
301313

302-
The panel accepts `theme` and `devtoolsProps` props. It creates a `div` element, mounts the core instance into it, and calls `unmount()` on cleanup.
314+
The panel constructs the core without arguments. It creates a `div` element, passes the complete `{ theme, devtoolsOpen }` plugin props to `mount()`, and registers `unmount()` with Svelte's component cleanup lifecycle.
303315

304316
## Angular
305317

‎docs/framework/react/adapter.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Each plugin describes a tab in the devtools panel:
2828
```ts
2929
type PluginRender =
3030
| JSX.Element
31-
| ((el: HTMLElement, theme: 'dark' | 'light') => JSX.Element)
31+
| ((el: HTMLElement, props: TanStackDevtoolsPluginProps) => JSX.Element)
3232

3333
type TanStackDevtoolsReactPlugin = {
3434
id?: string
@@ -38,7 +38,7 @@ type TanStackDevtoolsReactPlugin = {
3838
}
3939
```
4040
41-
- **`render`** can be a JSX element (simplest -- just pass `<YourPanel />`) or a function that receives the container element and current theme. The function form is useful when you need to access the raw DOM element or respond to theme changes.
41+
- **`render`** can be a JSX element (simplest -- just pass `<YourPanel />`) or a function that receives the container element and current `{ theme, devtoolsOpen }` props. The function form is useful when you need to access the raw DOM element or respond to plugin state changes.
4242
- **`name`** works the same way -- use a string for plain text, or JSX / a function for custom tab titles.
4343
- **`id`** is an optional unique identifier. If omitted, it is generated from the name.
4444
- **`defaultOpen`** marks the plugin as initially active when no other plugins are open.

‎docs/framework/svelte/adapter.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,16 @@ type TanStackDevtoolsSveltePlugin = {
3838
| Field | Type | Description |
3939
| --- | --- | --- |
4040
| `id` | `string` (optional) | Unique identifier for the plugin. |
41-
| `component` | `Component<any>` | The Svelte component to render as the plugin panel content. |
42-
| `name` | `string \| Component<any>` | Display name for the tab title. Can be a plain string or a Svelte component for custom rendering. |
43-
| `props` | `Record<string, any>` (optional) | Additional props passed to the plugin component on mount. |
41+
| `component` | `Component<any>` | The Svelte component to render as the plugin panel content. It receives the shared `theme` and `devtoolsOpen` props. |
42+
| `name` | `string \| Component<any>` | Display name for the tab title. A custom Svelte component receives the same merged plugin props. |
43+
| `props` | `Record<string, any>` (optional) | Additional props merged with the shared plugin props when the component mounts. |
4444
| `defaultOpen` | `boolean` (optional) | Whether this plugin tab should be open by default. |
4545
4646
## Key Difference from Other Frameworks
4747
48-
The Svelte adapter uses `component` (a Svelte component reference) instead of `render` (a JSX element) in plugin definitions. Props are provided through the `props` field and passed to the component via Svelte's `mount()` API, rather than being embedded directly in a JSX expression.
48+
The Svelte adapter uses `component` (a Svelte component reference) instead of `render` (a JSX element) in plugin definitions. It passes `{ theme, devtoolsOpen, ...plugin.props }` to the component through Svelte's `mount()` API.
49+
50+
When the core renders a plugin again, the adapter updates the existing Svelte host with the latest component and props. If the component identity is unchanged, its state and lifecycle remain intact. The adapter unmounts the host when the plugin is destroyed or the Devtools instance shuts down.
4951
5052
```svelte
5153
<!-- Svelte: pass component reference + props -->

‎docs/framework/svelte/basic-setup.md‎

Lines changed: 1 addition & 1 deletion

0 commit comments

Comments
 (0)