Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/devtools-utils-preact-11-unmount.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/devtools-utils': patch
---

Unmount the devtools core when a Preact panel unmounts on Preact 11. Preact 11 clears refs before effect cleanups run, so the panel skipped `unmount()`.
5 changes: 5 additions & 0 deletions .changeset/preact-devtools-plugin-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/preact-devtools': patch
---

Plugins, titles and custom triggers now see the context of the app that renders `<TanStackDevtools>`, so a plugin like `<PreactQueryDevtoolsPanel />` finds its `QueryClientProvider`. They used to render in a separate root with no context.
2 changes: 1 addition & 1 deletion e2e/apps/preact/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"dependencies": {
"@tanstack/devtools-e2e": "workspace:*",
"@tanstack/preact-devtools": "workspace:*",
"preact": "^10.28.0"
"preact": "^11.0.1"
},
"devDependencies": {
"@playwright/test": "^1.49.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/preact/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@tanstack/devtools-client": "0.0.8",
"@tanstack/devtools-event-client": "0.5.0",
"@tanstack/preact-devtools": "workspace:*",
"preact": "^10.28.0",
"preact": "^11.0.1",
"zod": "^4.3.5"
},
"devDependencies": {
Expand Down
22 changes: 11 additions & 11 deletions examples/preact/basic/src/package-json-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ export const PackageJsonPanel = () => {
style={{
background: color,
color: '#fff',
borderRadius: 4,
borderRadius: '4px',
padding: '1px 4px',
fontSize: 11,
fontSize: '11px',
}}
>
{text}
Expand All @@ -109,13 +109,13 @@ export const PackageJsonPanel = () => {
onClick={onClick}
style={{
padding: '2px 6px',
borderRadius: 5,
borderRadius: '5px',
border:
variant === 'primary' ? '1px solid #6d28d9' : '1px solid transparent',
cursor: 'pointer',
background: variant === 'primary' ? '#7c3aed' : 'transparent',
color: variant === 'primary' ? '#fff' : '#7c3aed',
fontSize: 12,
fontSize: '12px',
}}
>
{label}
Expand All @@ -141,11 +141,11 @@ export const PackageJsonPanel = () => {
const latest = info?.latest
const dt = info ? diffType(current, latest) : null
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span>{current}</span>
{dt && latest ? (
<span
style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}
style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }}
>
<span style={{ opacity: 0.6 }}>→</span>
{badge(`latest ${latest}`, diffColor[dt])}
Expand All @@ -166,7 +166,7 @@ export const PackageJsonPanel = () => {
| undefined
if (!info) return null
return (
<div style={{ display: 'flex', gap: 6 }}>
<div style={{ display: 'flex', gap: '6px' }}>
{btn('Wanted', () =>
(devtoolsEventClient as any).emit('upgrade-dependency', {
name,
Expand Down Expand Up @@ -205,7 +205,7 @@ export const PackageJsonPanel = () => {
const { wantedList, latestList } = makeLists(names)
if (wantedList.length === 0 && latestList.length === 0) return null
return (
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
{btn('All → wanted', () =>
(devtoolsEventClient as any).emit('upgrade-dependencies-bulk', {
list: wantedList,
Expand Down Expand Up @@ -234,7 +234,7 @@ export const PackageJsonPanel = () => {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 6,
gap: '6px',
}}
>
<h3 style={{ margin: 0, fontSize: 14 }}>{title}</h3>
Expand Down Expand Up @@ -285,9 +285,9 @@ export const PackageJsonPanel = () => {

return (
<div style={containerStyle}>
<h2 style={{ margin: '0 0 8px 0', fontSize: 16 }}>Package.json</h2>
<h2 style={{ margin: '0 0 8px 0', fontSize: '16px' }}>Package.json</h2>
{packageJson ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div style={sectionStyle}>
<h3 style={{ marginTop: 0, marginBottom: 6, fontSize: 14 }}>
Package info
Expand Down
2 changes: 1 addition & 1 deletion examples/preact/custom-devtools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"dependencies": {
"@tanstack/devtools-event-client": "0.5.0",
"@tanstack/preact-devtools": "workspace:*",
"preact": "^10.28.0"
"preact": "^11.0.1"
},
"devDependencies": {
"@preact/preset-vite": "^2.10.3",
Expand Down
1 change: 1 addition & 0 deletions packages/devtools-a11y/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"goober": "^2.1.16"
},
"devDependencies": {
"preact": "^11.0.1",
"vite-plugin-solid": "^2.11.11"
},
"peerDependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/devtools-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tanstack/devtools": "workspace:^",
"preact": "^11.0.1",
"svelte": "^5.0.0",
"tsup": "^8.5.0",
"tsup-preset-solid": "^2.2.0",
Expand Down
24 changes: 24 additions & 0 deletions packages/devtools-utils/src/preact/panel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { h, render } from 'preact'
import { act } from 'preact/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { createPreactPanel } from './panel'

describe('createPreactPanel', () => {
it('unmounts the core devtools when the panel unmounts', () => {
const mount = vi.fn()
const unmount = vi.fn()
const [Panel] = createPreactPanel(
class {
mount = mount
unmount = unmount
},
)
const host = document.createElement('div')

act(() => render(h(Panel, { theme: 'dark', devtoolsOpen: true }), host))
expect(mount).toHaveBeenCalledOnce()

act(() => render(null, host))
expect(unmount).toHaveBeenCalledOnce()
})
})
8 changes: 5 additions & 3 deletions packages/devtools-utils/src/preact/panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@ export function createPreactPanel<
useEffect(() => {
if (devtools.current) return
devtools.current = new CoreClass()
// Preact 11 clears the ref before this effect's cleanup runs.
const el = devToolRef.current

if (devToolRef.current) {
devtools.current.mount(devToolRef.current, props)
if (el) {
devtools.current.mount(el, props)
}

return () => {
if (devToolRef.current) {
if (el) {
devtools.current?.unmount()
devtools.current = null
}
Expand Down
2 changes: 1 addition & 1 deletion packages/preact-devtools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"devDependencies": {
"@preact/preset-vite": "^2.10.3",
"eslint-plugin-react-hooks": "^7.0.1",
"preact": "^10.28.0"
"preact": "^11.0.1"
},
"peerDependencies": {
"preact": ">=10.0.0"
Expand Down
40 changes: 29 additions & 11 deletions packages/preact-devtools/src/devtools.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
import { render } from 'preact'
import { TanStackDevtoolsCore } from '@tanstack/devtools'
import type { JSX } from 'preact'
import type { ComponentChildren, JSX } from 'preact'
import type {
ClientEventBusConfig,
TanStackDevtoolsConfig,
Expand Down Expand Up @@ -116,20 +116,38 @@ export interface TanStackDevtoolsPreactInit {
eventBusConfig?: ClientEventBusConfig
}

// Simple portal component for Preact
function Portal({
children,
container,
}: {
children: JSX.Element
container: HTMLElement
}) {
// Hands the portal's context to the separate root, so plugins can read the app's providers.
function ContextProvider(
this: { getChildContext?: () => object },
props: { context: object; children: ComponentChildren },
) {
this.getChildContext = () => props.context
return props.children
}

// Core `createPortal` only exists in Preact 11, and `preact/compat` patches global options.
function Portal(
{
children,
container,
}: {
children: JSX.Element
container: HTMLElement
},
context: object,
) {
useEffect(() => {
render(
<ContextProvider context={context}>{children}</ContextProvider>,
container,
)
})

useEffect(() => {
render(children, container)
return () => {
render(null, container)
}
}, [children, container])
}, [container])

return null
}
Expand Down
81 changes: 81 additions & 0 deletions packages/preact-devtools/tests/devtools.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { createContext, render } from 'preact'
import { useContext, useState } from 'preact/hooks'
import { act } from 'preact/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { TanStackDevtoolsCore } from '@tanstack/devtools'
import { TanStackDevtools } from '../src/devtools'
import type { TanStackDevtoolsPlugin } from '@tanstack/devtools'

// The real core does a full Solid mount. The adapter only needs these methods.
vi.mock('@tanstack/devtools', () => ({
TanStackDevtoolsCore: vi.fn(
class {
mount = vi.fn()
unmount = vi.fn()
setConfig = vi.fn()
},
),
}))

const CoreMock = vi.mocked(TanStackDevtoolsCore)

const AppContext = createContext('default')

function Consumer() {
return <span data-testid="value">{useContext(AppContext)}</span>
}

describe('TanStackDevtools (Preact adapter)', () => {
const host = document.createElement('div')

afterEach(() => {
act(() => {
render(null, host)
})
document.body.innerHTML = ''
CoreMock.mockClear()
})

it('renders plugins with the context of the app', () => {
let setValue: (value: string) => void = () => {}

function App() {
const [value, set] = useState('from app')
setValue = set
return (
<AppContext.Provider value={value}>
<TanStackDevtools
plugins={[{ id: 'p', name: 'P', render: <Consumer /> }]}
/>
</AppContext.Provider>
)
}

act(() => {
render(<App />, host)
})

const { plugins } = CoreMock.mock.calls[0]![0] as {
plugins: Array<TanStackDevtoolsPlugin>
}
// The adapter only portals into containers it can find by id.
const container = document.createElement('div')
container.id = 'p'
document.body.appendChild(container)

act(() => {
plugins[0]!.render(container, { theme: 'dark', devtoolsOpen: true })
})
expect(container.textContent).toBe('from app')

act(() => {
setValue('updated')
})
expect(container.textContent).toBe('updated')

act(() => {
render(null, host)
})
expect(container.textContent).toBe('')
})
})
Loading
Loading