Skip to content

Commit adfd798

Browse files
committed
fix(query-devtools): isolate state per devtools instance
Module-level SolidJS signals and cache maps were shared across all mounted devtools, so selecting a query, resizing, or toggling offline in one instance leaked into another. Move them into a per-instance context provider so each devtools tree owns its own reactive state and cache batching. Closes #9681
1 parent 86bb8a6 commit adfd798

6 files changed

Lines changed: 350 additions & 33 deletions

File tree

.changeset/heavy-carpets-divide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tanstack/query-devtools": patch
3+
---
4+
5+
Fix devtools UI state (selected query/mutation, panel width, offline mock toggle, and query/mutation cache subscriptions) leaking between independently mounted devtools instances. Selecting a query, resizing a panel, or mocking offline behavior in one devtools instance no longer affects another instance mounted on a different `QueryClient`.

packages/query-devtools/src/Devtools.tsx

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,13 @@ import {
5151
XCircle,
5252
} from './icons'
5353
import Explorer from './Explorer'
54-
import { usePiPWindow, useQueryDevtoolsContext, useTheme } from './contexts'
54+
import {
55+
DevtoolsInstanceProvider,
56+
useDevtoolsInstanceContext,
57+
usePiPWindow,
58+
useQueryDevtoolsContext,
59+
useTheme,
60+
} from './contexts'
5561
import {
5662
BUTTON_POSITION,
5763
DEFAULT_HEIGHT,
@@ -78,7 +84,7 @@ import type {
7884
QueryCacheNotifyEvent,
7985
} from '@tanstack/query-core'
8086
import type { StorageObject, StorageSetter } from '@solid-primitives/storage'
81-
import type { Accessor, Component, JSX, Setter } from 'solid-js'
87+
import type { Accessor, Component, JSX } from 'solid-js'
8288

8389
interface DevtoolsPanelProps {
8490
localStore: StorageObject<string>
@@ -98,20 +104,20 @@ interface QueryStatusProps {
98104
count: number
99105
}
100106

101-
const [selectedQueryHash, setSelectedQueryHash] = createSignal<string | null>(
102-
null,
103-
)
104-
const [selectedMutationId, setSelectedMutationId] = createSignal<number | null>(
105-
null,
106-
)
107-
const [panelWidth, setPanelWidth] = createSignal(0)
108-
const [offline, setOffline] = createSignal(false)
109-
110107
export type DevtoolsComponentType = Component<QueryDevtoolsProps> & {
111108
shadowDOMTarget?: ShadowRoot
112109
}
113110

114111
export const Devtools: Component<DevtoolsPanelProps> = (props) => {
112+
return (
113+
<DevtoolsInstanceProvider>
114+
<DevtoolsImpl {...props} />
115+
</DevtoolsInstanceProvider>
116+
)
117+
}
118+
119+
const DevtoolsImpl: Component<DevtoolsPanelProps> = (props) => {
120+
const { setOffline } = useDevtoolsInstanceContext()
115121
const theme = useTheme()
116122
const css = useQueryDevtoolsContext().shadowDOMTarget
117123
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
@@ -281,6 +287,7 @@ export const Devtools: Component<DevtoolsPanelProps> = (props) => {
281287
const PiPPanel: Component<{
282288
children: JSX.Element
283289
}> = (props) => {
290+
const { panelWidth, setPanelWidth } = useDevtoolsInstanceContext()
284291
const pip = usePiPWindow()
285292
const theme = useTheme()
286293
const css = useQueryDevtoolsContext().shadowDOMTarget
@@ -351,6 +358,7 @@ const PiPPanel: Component<{
351358
export const ParentPanel: Component<{
352359
children: JSX.Element
353360
}> = (props) => {
361+
const { panelWidth, setPanelWidth } = useDevtoolsInstanceContext()
354362
const theme = useTheme()
355363
const css = useQueryDevtoolsContext().shadowDOMTarget
356364
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
@@ -408,6 +416,8 @@ export const ParentPanel: Component<{
408416
}
409417

410418
const DraggablePanel: Component<DevtoolsPanelProps> = (props) => {
419+
const { panelWidth, setPanelWidth, setSelectedQueryHash } =
420+
useDevtoolsInstanceContext()
411421
const theme = useTheme()
412422
const css = useQueryDevtoolsContext().shadowDOMTarget
413423
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
@@ -672,6 +682,14 @@ const DraggablePanel: Component<DevtoolsPanelProps> = (props) => {
672682
}
673683

674684
export const ContentView: Component<ContentViewProps> = (props) => {
685+
const {
686+
panelWidth,
687+
offline,
688+
selectedQueryHash,
689+
selectedMutationId,
690+
setSelectedQueryHash,
691+
setSelectedMutationId,
692+
} = useDevtoolsInstanceContext()
675693
setupQueryCacheSubscription()
676694
setupMutationCacheSubscription()
677695
let containerRef!: HTMLDivElement
@@ -1372,6 +1390,8 @@ export const ContentView: Component<ContentViewProps> = (props) => {
13721390
}
13731391

13741392
const QueryRow: Component<{ query: Query }> = (props) => {
1393+
const { selectedQueryHash, setSelectedQueryHash } =
1394+
useDevtoolsInstanceContext()
13751395
const theme = useTheme()
13761396
const css = useQueryDevtoolsContext().shadowDOMTarget
13771397
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
@@ -1481,6 +1501,8 @@ const QueryRow: Component<{ query: Query }> = (props) => {
14811501
}
14821502

14831503
const MutationRow: Component<{ mutation: Mutation }> = (props) => {
1504+
const { selectedMutationId, setSelectedMutationId } =
1505+
useDevtoolsInstanceContext()
14841506
const theme = useTheme()
14851507
const css = useQueryDevtoolsContext().shadowDOMTarget
14861508
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
@@ -1722,6 +1744,7 @@ const MutationStatusCount: Component = () => {
17221744
}
17231745

17241746
const QueryStatus: Component<QueryStatusProps> = (props) => {
1747+
const { panelWidth, selectedQueryHash } = useDevtoolsInstanceContext()
17251748
const theme = useTheme()
17261749
const css = useQueryDevtoolsContext().shadowDOMTarget
17271750
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
@@ -1837,6 +1860,8 @@ const QueryStatus: Component<QueryStatusProps> = (props) => {
18371860
}
18381861

18391862
const QueryDetails = () => {
1863+
const { selectedQueryHash, setSelectedQueryHash } =
1864+
useDevtoolsInstanceContext()
18401865
const theme = useTheme()
18411866
const css = useQueryDevtoolsContext().shadowDOMTarget
18421867
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
@@ -2382,6 +2407,7 @@ const QueryDetails = () => {
23822407
}
23832408

23842409
const MutationDetails = () => {
2410+
const { selectedMutationId } = useDevtoolsInstanceContext()
23852411
const theme = useTheme()
23862412
const css = useQueryDevtoolsContext().shadowDOMTarget
23872413
? goober.css.bind({ target: useQueryDevtoolsContext().shadowDOMTarget })
@@ -2569,15 +2595,8 @@ const MutationDetails = () => {
25692595
)
25702596
}
25712597

2572-
const queryCacheMap = new Map<
2573-
(q: Accessor<QueryCache>) => any,
2574-
{
2575-
setter: Setter<any>
2576-
shouldUpdate: (event: QueryCacheNotifyEvent) => boolean
2577-
}
2578-
>()
2579-
25802598
const setupQueryCacheSubscription = () => {
2599+
const { queryCacheMap } = useDevtoolsInstanceContext()
25812600
const queryCache = createMemo(() => {
25822601
const client = useQueryDevtoolsContext().client
25832602
return client.getQueryCache()
@@ -2605,6 +2624,7 @@ const createSubscribeToQueryCacheBatcher = <T,>(
26052624
equalityCheck: boolean = true,
26062625
shouldUpdate: (event: QueryCacheNotifyEvent) => boolean = () => true,
26072626
) => {
2627+
const { queryCacheMap } = useDevtoolsInstanceContext()
26082628
const queryCache = createMemo(() => {
26092629
const client = useQueryDevtoolsContext().client
26102630
return client.getQueryCache()
@@ -2631,12 +2651,8 @@ const createSubscribeToQueryCacheBatcher = <T,>(
26312651
return value
26322652
}
26332653

2634-
const mutationCacheMap = new Map<
2635-
(q: Accessor<MutationCache>) => any,
2636-
Setter<any>
2637-
>()
2638-
26392654
const setupMutationCacheSubscription = () => {
2655+
const { mutationCacheMap } = useDevtoolsInstanceContext()
26402656
const mutationCache = createMemo(() => {
26412657
const client = useQueryDevtoolsContext().client
26422658
return client.getMutationCache()
@@ -2662,6 +2678,7 @@ const createSubscribeToMutationCacheBatcher = <T,>(
26622678
callback: (queryCache: Accessor<MutationCache>) => Exclude<T, Function>,
26632679
equalityCheck: boolean = true,
26642680
) => {
2681+
const { mutationCacheMap } = useDevtoolsInstanceContext()
26652682
const mutationCache = createMemo(() => {
26662683
const client = useQueryDevtoolsContext().client
26672684
return client.getMutationCache()

packages/query-devtools/src/DevtoolsPanelComponent.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { createMemo } from 'solid-js'
33
import { ContentView, ParentPanel } from './Devtools'
44
import { getPreferredColorScheme } from './utils'
55
import { THEME_PREFERENCE } from './constants'
6-
import { PiPProvider, QueryDevtoolsContext, ThemeContext } from './contexts'
6+
import {
7+
DevtoolsInstanceProvider,
8+
PiPProvider,
9+
QueryDevtoolsContext,
10+
ThemeContext,
11+
} from './contexts'
712
import type { Theme } from './contexts'
813
import type { DevtoolsComponentType } from './Devtools'
914

@@ -30,14 +35,16 @@ const DevtoolsPanelComponent: DevtoolsComponentType = (props) => {
3035
setLocalStore={setLocalStore}
3136
>
3237
<ThemeContext.Provider value={theme}>
33-
<ParentPanel>
34-
<ContentView
35-
localStore={localStore}
36-
setLocalStore={setLocalStore}
37-
onClose={props.onClose}
38-
showPanelViewOnly
39-
/>
40-
</ParentPanel>
38+
<DevtoolsInstanceProvider>
39+
<ParentPanel>
40+
<ContentView
41+
localStore={localStore}
42+
setLocalStore={setLocalStore}
43+
onClose={props.onClose}
44+
showPanelViewOnly
45+
/>
46+
</ParentPanel>
47+
</DevtoolsInstanceProvider>
4148
</ThemeContext.Provider>
4249
</PiPProvider>
4350
</QueryDevtoolsContext.Provider>

0 commit comments

Comments
 (0)