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
13 changes: 13 additions & 0 deletions .changeset/fix-vue-query-options-query-key-reactive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@tanstack/vue-query': patch
---

fix(vue-query): allow reactive and getter values as `queryKey` property in `queryOptions`

This fixes a regression introduced in #10452 where `queryOptions` only accepted plain arrays for the `queryKey` property, but not `computed` refs, `Ref` values, or getter functions. The related fix in #10465 only covered the `enabled` property.

Now the `queryKey` property in `queryOptions` accepts the same reactive forms as `enabled`:
- Plain `QueryKey` arrays
- `Ref<QueryKey>`
- `ComputedRef<QueryKey>`
- `() => QueryKey` (getter)
5 changes: 5 additions & 0 deletions .changeset/vue-devtools-class-attribute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/vue-query-devtools': patch
---

Use the Vue `class` attribute for devtools template containers.
130 changes: 130 additions & 0 deletions packages/query-devtools/src/__tests__/Devtools.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,39 @@ describe('Devtools', () => {
'pending',
)
})

it('should restore the previous query options when "Restore Loading" is clicked after "Trigger Loading"', async () => {
const queryFn = vi.fn(() => Promise.resolve('original'))
queryClient.prefetchQuery({
queryKey: ['action-restore-loading'],
queryFn,
})
await vi.advanceTimersByTimeAsync(0)
expect(queryFn).toHaveBeenCalledTimes(1)

const rendered = renderDevtools({ initialIsOpen: true })

fireEvent.click(
rendered.getByLabelText(/Query key \["action-restore-loading"\]/),
)

// First click puts the query into a pending state with `data: undefined`
// and stashes the original options in `fetchMeta.__previousQueryOptions`.
fireEvent.click(rendered.getByText('Trigger Loading'))
expect(
queryClient.getQueryState(['action-restore-loading'])?.status,
).toBe('pending')

// Second click runs `restoreQueryAfterLoadingOrError`, which cancels the
// never-resolving fetch and refetches with the stashed options.
fireEvent.click(rendered.getByText('Restore Loading'))
await vi.advanceTimersByTimeAsync(0)

expect(queryFn).toHaveBeenCalledTimes(2)
expect(queryClient.getQueryData(['action-restore-loading'])).toBe(
'original',
)
})
})

describe('mutation details', () => {
Expand Down Expand Up @@ -1076,6 +1109,103 @@ describe('Devtools', () => {
Number(localStorage.getItem('TanstackQueryDevtools.width')),
).toBeGreaterThan(initialWidth)
})

it('should clamp the width to the minimum when dragging shrinks the panel below the minimum width', () => {
const initialWidth = 200
const rendered = renderDevtools(
{ position: 'left', initialIsOpen: true },
{ 'TanstackQueryDevtools.width': String(initialWidth) },
)

const handle = rendered.getByLabelText('Resize devtools panel')
const panel = handle.parentElement
expect(panel).toBeInstanceOf(HTMLElement)
// `width` is read twice during drag: once as the base size, and again
// after the clamp to detect when the panel has hit its minimum. The
// first call returns `initialWidth`; the second returns `0` so the
// `localStore.width < newWidth` restore branch stays inactive and only
// the `newSize < minWidth` clamp is observed.
const getBoundingClientRect = vi
.spyOn(panel!, 'getBoundingClientRect')
.mockReturnValueOnce({
height: 0,
width: initialWidth,
x: 0,
y: 0,
top: 0,
right: 0,
bottom: 0,
left: 0,
toJSON: () => ({}),
})
getBoundingClientRect.mockReturnValue({
height: 0,
width: 0,
x: 0,
y: 0,
top: 0,
right: 0,
bottom: 0,
left: 0,
toJSON: () => ({}),
})

// In `left` position, dragging the cursor left (`clientX` 100 → 0)
// shrinks the panel by 100px, which lands well under the 192px minimum.
fireEvent.mouseDown(handle, { clientX: 100, clientY: 0 })
fireEvent(
document,
new MouseEvent('mousemove', { clientX: 0, clientY: 0 }),
)
fireEvent(document, new MouseEvent('mouseup'))

expect(Number(localStorage.getItem('TanstackQueryDevtools.width'))).toBe(
192,
)
})

it('should close the query details panel when dragging shrinks the panel below the minimum height', () => {
queryClient.setQueryData(['shrink-below-min-height'], [{ id: 1 }])
const rendered = renderDevtools({
position: 'bottom',
initialIsOpen: true,
})

// Open the query details so `selectedQueryHash` is set.
fireEvent.click(
rendered.getByLabelText(/Query key \["shrink-below-min-height"\]/),
)
expect(rendered.getByText('Query Details')).toBeInTheDocument()

const handle = rendered.getByLabelText('Resize devtools panel')
const panel = handle.parentElement
expect(panel).toBeInstanceOf(HTMLElement)
// Stub the base size to a value just above the 56px (`3.5rem`) minimum so
// a small downward drag pushes `newSize` below `minHeight` and triggers
// the clamp branch that also resets `selectedQueryHash`.
vi.spyOn(panel!, 'getBoundingClientRect').mockReturnValue({
height: 60,
width: 0,
x: 0,
y: 0,
top: 0,
right: 0,
bottom: 0,
left: 0,
toJSON: () => ({}),
})

// In `bottom` position, dragging the cursor down (`clientY` 100 → 200)
// shrinks the panel by 100px, which is well under the 56px minimum.
fireEvent.mouseDown(handle, { clientX: 0, clientY: 100 })
fireEvent(
document,
new MouseEvent('mousemove', { clientX: 0, clientY: 200 }),
)
fireEvent(document, new MouseEvent('mouseup'))

expect(rendered.queryByText('Query Details')).not.toBeInTheDocument()
})
})

describe('online toggle', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/vue-query-devtools/src/devtools.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,5 @@ onMounted(() => {
</script>

<template>
<div className="tsqd-parent-container" ref="div"></div>
<div class="tsqd-parent-container" ref="div"></div>
</template>
2 changes: 1 addition & 1 deletion packages/vue-query-devtools/src/devtoolsPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ onMounted(() => {
</script>

<template>
<div :style="style" className="tsqd-parent-container" ref="div"></div>
<div :style="style" class="tsqd-parent-container" ref="div"></div>
</template>
34 changes: 34 additions & 0 deletions packages/vue-query/src/__tests__/queryOptions.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,4 +298,38 @@ describe('queryOptions', () => {

expectTypeOf(options.queryKey).not.toBeUndefined()
})

it('should allow computed ref as queryKey', () => {
const id = ref<string | null>('1')

// This was broken in #10452, the #10465 fix only covered `enabled`
const options = queryOptions({
queryKey: computed(() => ['foo', id.value] as const),
queryFn: () => Promise.resolve({ id: '1' }),
})

expectTypeOf(options.queryKey).not.toBeUndefined()
})

it('should allow ref as queryKey', () => {
const keyRef = ref(['foo', '1'] as const)

const options = queryOptions({
queryKey: keyRef,
queryFn: () => Promise.resolve({ id: '1' }),
})

expectTypeOf(options.queryKey).not.toBeUndefined()
})

it('should allow getter function as queryKey', () => {
const id = ref<string | null>('1')

const options = queryOptions({
queryKey: () => ['foo', id.value] as const,
queryFn: () => Promise.resolve({ id: '1' }),
})

expectTypeOf(options.queryKey).not.toBeUndefined()
})
})
16 changes: 9 additions & 7 deletions packages/vue-query/src/queryOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ export type QueryOptions<
TQueryData,
DeepUnwrapRef<TQueryKey>
>)
: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
DeepUnwrapRef<TQueryKey>
>[Property]
: Property extends 'queryKey'
? MaybeRefOrGetter<TQueryKey>
: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
DeepUnwrapRef<TQueryKey>
>[Property]
} & ShallowOption

export type UndefinedInitialQueryOptions<
Expand Down
Loading