Skip to content
22 changes: 20 additions & 2 deletions src/core/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,13 +507,31 @@ export class QueryObserver<
} else {
placeholderData =
typeof options.placeholderData === 'function'
? (options.placeholderData as PlaceholderDataFunction<TData>)()
? (options.placeholderData as PlaceholderDataFunction<TQueryData>)()
: options.placeholderData
if (options.select && typeof placeholderData !== 'undefined') {
try {
placeholderData = options.select(placeholderData)
if (options.structuralSharing !== false) {
placeholderData = replaceEqualDeep(
prevResultOptions?.placeholderData,
placeholderData
)
}
this.previousSelectError = null
} catch (selectError) {
getLogger().error(selectError)
error = selectError
this.previousSelectError = selectError
errorUpdatedAt = Date.now()
status = 'error'
}
}
}

if (typeof placeholderData !== 'undefined') {
status = 'success'
data = placeholderData
data = placeholderData as TData
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure why I need the type assertion here - not really happy with it. placeholderData is of type TData | TQueryFnData - maybe I have the typings wrong somewhere @boschni?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think there is no way around this. If there is no select function or behavior then placeholderData should be of type TData but TS does not know if this is the case upfront

isPlaceholderData = true
}
}
Expand Down
4 changes: 1 addition & 3 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ export interface QueryFunctionContext<

export type InitialDataFunction<T> = () => T | undefined

export type InitialStaleFunction = () => boolean

export type PlaceholderDataFunction<TResult> = () => TResult | undefined

export type QueryKeyHashFunction<TQueryKey extends QueryKey> = (
Expand Down Expand Up @@ -183,7 +181,7 @@ export interface QueryObserverOptions<
/**
* If set, this value will be used as the placeholder data for this particular query observer while the query is still in the `loading` data and no initialData has been provided.
*/
placeholderData?: TData | PlaceholderDataFunction<TData>
placeholderData?: TQueryData | PlaceholderDataFunction<TQueryData>
/**
* If set, the observer will optimistically set the result in fetching state before the query has actually started fetching.
* This is to make sure the results are not lagging behind.
Expand Down
89 changes: 89 additions & 0 deletions src/react/tests/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3437,6 +3437,95 @@ describe('useQuery', () => {
])
})

it('placeholder data should run through select', async () => {
const key1 = queryKey()

const states: UseQueryResult<string>[] = []

function Page() {
const state = useQuery(key1, () => 1, {
placeholderData: 23,
select: data => String(data * 2),
})

states.push(state)

return (
<div>
<h2>Data: {state.data}</h2>
<div>Status: {state.status}</div>
</div>
)
}

const rendered = renderWithClient(queryClient, <Page />)
await waitFor(() => rendered.getByText('Data: 2'))

expect(states).toMatchObject([
{
isSuccess: true,
isPlaceholderData: true,
data: '46',
},
{
isSuccess: true,
isPlaceholderData: false,
data: '2',
},
])
})

it('placeholder data function result should run through select', async () => {
const key1 = queryKey()

const states: UseQueryResult<string>[] = []
let placeholderFunctionRunCount = 0

function Page() {
const state = useQuery(key1, () => 1, {
placeholderData: () => {
placeholderFunctionRunCount++
return 23
},
select: data => String(data * 2),
})

states.push(state)

return (
<div>
<h2>Data: {state.data}</h2>
<div>Status: {state.status}</div>
</div>
)
}

const rendered = renderWithClient(queryClient, <Page />)
await waitFor(() => rendered.getByText('Data: 2'))

rendered.rerender(<Page />)

expect(states).toMatchObject([
{
isSuccess: true,
isPlaceholderData: true,
data: '46',
},
{
isSuccess: true,
isPlaceholderData: false,
data: '2',
},
{
isSuccess: true,
isPlaceholderData: false,
data: '2',
},
])

expect(placeholderFunctionRunCount).toEqual(1)
})

it('should cancel the query function when there are no more subscriptions', async () => {
const key = queryKey()
let cancelFn: jest.Mock = jest.fn()
Expand Down