diff --git a/.changeset/query-devtools-missing-state.md b/.changeset/query-devtools-missing-state.md new file mode 100644 index 00000000000..b50b9bdf7ab --- /dev/null +++ b/.changeset/query-devtools-missing-state.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-devtools': patch +--- + +Avoid crashing devtools query rows when a cached query state is temporarily unavailable. diff --git a/.changeset/stable-devtools-query-rows.md b/.changeset/stable-devtools-query-rows.md new file mode 100644 index 00000000000..d58a0a9dbc0 --- /dev/null +++ b/.changeset/stable-devtools-query-rows.md @@ -0,0 +1,5 @@ +--- +"@tanstack/query-devtools": patch +--- + +Resolve devtools query rows from their stable query hash so mutated object query keys do not break row rendering. diff --git a/packages/query-devtools/src/Devtools.tsx b/packages/query-devtools/src/Devtools.tsx index 8e9b1f4a321..dfb66e496a7 100644 --- a/packages/query-devtools/src/Devtools.tsx +++ b/packages/query-devtools/src/Devtools.tsx @@ -1384,61 +1384,41 @@ const QueryRow: Component<{ query: Query }> = (props) => { const t = (light: string, dark: string) => (theme() === 'dark' ? dark : light) const queryState = createSubscribeToQueryCacheBatcher( - (queryCache) => - queryCache().find({ - queryKey: props.query.queryKey, - })?.state, + (queryCache) => queryCache().get(props.query.queryHash)?.state, true, (e) => e.query.queryHash === props.query.queryHash, ) const isDisabled = createSubscribeToQueryCacheBatcher( (queryCache) => - queryCache() - .find({ - queryKey: props.query.queryKey, - }) - ?.isDisabled() ?? false, + queryCache().get(props.query.queryHash)?.isDisabled() ?? false, true, (e) => e.query.queryHash === props.query.queryHash, ) const isStatic = createSubscribeToQueryCacheBatcher( (queryCache) => - queryCache() - .find({ - queryKey: props.query.queryKey, - }) - ?.isStatic() ?? false, + queryCache().get(props.query.queryHash)?.isStatic() ?? false, true, (e) => e.query.queryHash === props.query.queryHash, ) const isStale = createSubscribeToQueryCacheBatcher( - (queryCache) => - queryCache() - .find({ - queryKey: props.query.queryKey, - }) - ?.isStale() ?? false, + (queryCache) => queryCache().get(props.query.queryHash)?.isStale() ?? false, true, (e) => e.query.queryHash === props.query.queryHash, ) const observers = createSubscribeToQueryCacheBatcher( (queryCache) => - queryCache() - .find({ - queryKey: props.query.queryKey, - }) - ?.getObserversCount() ?? 0, + queryCache().get(props.query.queryHash)?.getObserversCount() ?? 0, true, (e) => e.query.queryHash === props.query.queryHash, ) const color = createMemo(() => getQueryStatusColor({ - queryState: queryState()!, + queryState: queryState(), observerCount: observers(), isStale: isStale(), }), diff --git a/packages/query-devtools/src/__tests__/Devtools.test.tsx b/packages/query-devtools/src/__tests__/Devtools.test.tsx index 066f71268ab..864698429c1 100644 --- a/packages/query-devtools/src/__tests__/Devtools.test.tsx +++ b/packages/query-devtools/src/__tests__/Devtools.test.tsx @@ -390,6 +390,18 @@ describe('Devtools', () => { expect(rendered.getByText('static')).toBeInTheDocument() expect(rendered.getByLabelText(/, static/)).toBeInTheDocument() }) + + it('should render a query row when an object query key is mutated in place', () => { + const filters = { page: 1 } + queryClient.setQueryData(['mutable-key', filters], 'x') + filters.page = 2 + + const rendered = renderDevtools({ initialIsOpen: true }) + + expect( + rendered.getByLabelText(/Query key \["mutable-key",\{"page":1\}\]/), + ).toBeInTheDocument() + }) }) describe('picture-in-picture', () => { diff --git a/packages/query-devtools/src/__tests__/Explorer.test.tsx b/packages/query-devtools/src/__tests__/Explorer.test.tsx index fb329a98db3..9a4940b1c60 100644 --- a/packages/query-devtools/src/__tests__/Explorer.test.tsx +++ b/packages/query-devtools/src/__tests__/Explorer.test.tsx @@ -28,7 +28,11 @@ describe('Explorer', () => { queryClient.clear() }) - function renderExplorer(props: Parameters[0]) { + function renderExplorer( + props: Parameters[0], + options: { theme?: 'dark' | 'light' } = {}, + ) { + const theme = options.theme ?? 'dark' return render(() => ( { onlineManager, }} > - 'dark'}> + theme}> @@ -562,4 +566,26 @@ describe('Explorer', () => { expect(queryClient.getQueryData(['data'])).toEqual({}) }) }) + + describe('theme', () => { + it('should render without throwing under the "light" theme', () => { + const value = { items: ['a'], flag: true } + queryClient.setQueryData(['data'], value) + + expect(() => + renderExplorer( + { + label: 'Data', + value, + defaultExpanded: ['Data'], + editable: true, + activeQuery: queryClient + .getQueryCache() + .find({ queryKey: ['data'] }) as Query, + }, + { theme: 'light' }, + ), + ).not.toThrow() + }) + }) }) diff --git a/packages/query-devtools/src/__tests__/utils.test.ts b/packages/query-devtools/src/__tests__/utils.test.ts index 07dff9ece4d..511553e2bb8 100644 --- a/packages/query-devtools/src/__tests__/utils.test.ts +++ b/packages/query-devtools/src/__tests__/utils.test.ts @@ -1573,6 +1573,16 @@ describe('Utils tests', () => { ).toBe('gray') }) + it('should return "gray" when the query state is unavailable', () => { + expect( + getQueryStatusColor({ + queryState: undefined, + observerCount: 0, + isStale: false, + }), + ).toBe('gray') + }) + it('should return "purple" when fetchStatus is "paused"', () => { expect( getQueryStatusColor({ diff --git a/packages/query-devtools/src/utils.tsx b/packages/query-devtools/src/utils.tsx index 9a826cd21e3..5306f2cf5f2 100644 --- a/packages/query-devtools/src/utils.tsx +++ b/packages/query-devtools/src/utils.tsx @@ -31,15 +31,15 @@ export function getQueryStatusColor({ observerCount, isStale, }: { - queryState: Query['state'] + queryState: Query['state'] | undefined observerCount: number isStale: boolean }) { - return queryState.fetchStatus === 'fetching' + return queryState?.fetchStatus === 'fetching' ? 'blue' : !observerCount ? 'gray' - : queryState.fetchStatus === 'paused' + : queryState?.fetchStatus === 'paused' ? 'purple' : isStale ? 'yellow'