diff --git a/.changeset/placeholder-select-fix.md b/.changeset/placeholder-select-fix.md new file mode 100644 index 00000000000..939d6ca6f1f --- /dev/null +++ b/.changeset/placeholder-select-fix.md @@ -0,0 +1,5 @@ +--- +"@tanstack/query-core": patch +--- + +Recompute selected placeholder data when the selector changes or is removed. diff --git a/packages/query-core/src/__tests__/queryObserver.test.tsx b/packages/query-core/src/__tests__/queryObserver.test.tsx index 594aa1a67da..8cde8480ff0 100644 --- a/packages/query-core/src/__tests__/queryObserver.test.tsx +++ b/packages/query-core/src/__tests__/queryObserver.test.tsx @@ -24,6 +24,40 @@ describe('queryObserver', () => { vi.useRealTimers() }) + it.each([false, true])( + 'should update selected placeholder data when select changes (function: %s)', + (functional) => { + const key = queryKey() + const placeholderData = functional ? () => 2 : 2 + const options = { + queryKey: key, + queryFn: () => 2, + enabled: false, + placeholderData, + } + const observer = new QueryObserver(queryClient, { + ...options, + select: (value) => value * 2, + }) + expect(observer.getCurrentResult().data).toBe(4) + observer.setOptions({ ...options, select: (value) => value * 3 }) + expect(observer.getCurrentResult()).toMatchObject({ + data: 6, + isPlaceholderData: true, + }) + observer.setOptions(options) + expect(observer.getCurrentResult()).toMatchObject({ + data: 2, + isPlaceholderData: true, + }) + observer.setOptions({ ...options, select: (value) => value * 4 }) + expect(observer.getCurrentResult()).toMatchObject({ + data: 8, + isPlaceholderData: true, + }) + }, + ) + it('should trigger a fetch when subscribed', () => { const key = queryKey() const queryFn = vi diff --git a/packages/query-core/src/queryObserver.ts b/packages/query-core/src/queryObserver.ts index beb3a2370bc..9f7e8a2883e 100644 --- a/packages/query-core/src/queryObserver.ts +++ b/packages/query-core/src/queryObserver.ts @@ -620,7 +620,8 @@ export class QueryObserver< // Memoize placeholder data if ( prevResult?.isPlaceholderData && - options.placeholderData === prevResultOptions?.placeholderData + options.placeholderData === prevResultOptions?.placeholderData && + options.select === prevResultOptions.select ) { placeholderData = prevResult.data // we have to skip select when reading this memoization diff --git a/packages/react-query/src/__tests__/useQuery.test.tsx b/packages/react-query/src/__tests__/useQuery.test.tsx index 1c706a8ab21..0e5fe66f35f 100644 --- a/packages/react-query/src/__tests__/useQuery.test.tsx +++ b/packages/react-query/src/__tests__/useQuery.test.tsx @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { act, fireEvent, render } from '@testing-library/react' +import { act, fireEvent, render, renderHook } from '@testing-library/react' import * as React from 'react' import { ErrorBoundary } from 'react-error-boundary' import { @@ -44,6 +44,35 @@ describe('useQuery', () => { vi.useRealTimers() }) + it.each([false, true])( + 'should update placeholder selection on rerender (StrictMode: %s)', + (strict) => { + const key = queryKey() + const initialProps: { factor: number | undefined } = { factor: 2 } + const view = renderHook( + ({ factor }: { factor: number | undefined }) => + useQuery( + { + queryKey: key, + queryFn: () => 2, + enabled: false, + placeholderData: 2, + select: + factor === undefined ? undefined : (value) => value * factor, + }, + queryClient, + ), + { initialProps, wrapper: strict ? React.StrictMode : undefined }, + ) + expect(view.result.current.data).toBe(4) + view.rerender({ factor: 3 }) + expect(view.result.current.data).toBe(6) + view.rerender({ factor: undefined }) + expect(view.result.current.data).toBe(2) + view.unmount() + }, + ) + // See https://github.com/tannerlinsley/react-query/issues/105 it('should allow to set default data value', async () => { const key = queryKey()