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
23 changes: 18 additions & 5 deletions src/core/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { Query, QueryState, Action, FetchOptions } from './query'
import type { QueryClient } from './queryClient'
import { focusManager } from './focusManager'
import { Subscribable } from './subscribable'
import { getLogger } from './logger'

type QueryObserverListener<TData, TError> = (
result: QueryObserverResult<TData, TError>
Expand Down Expand Up @@ -240,6 +241,9 @@ export class QueryObserver<
this.updateRefetchInterval()
}
}

// Reset previous options after all code related to option changes has run
this.previousOptions = this.options
}

getCurrentResult(): QueryObserverResult<TData, TError> {
Expand Down Expand Up @@ -390,6 +394,8 @@ export class QueryObserver<
let isPlaceholderData = false
let data: TData | undefined
let dataUpdatedAt = state.dataUpdatedAt
let error = state.error
let errorUpdatedAt = state.errorUpdatedAt

// Optimistically set status to loading if we will start fetching
if (!this.hasListeners() && this.willFetchOnMount()) {
Expand Down Expand Up @@ -421,9 +427,16 @@ export class QueryObserver<
) {
data = this.currentResult.data
} else {
data = this.options.select(state.data)
if (this.options.structuralSharing !== false) {
data = replaceEqualDeep(this.currentResult?.data, data)
try {
data = this.options.select(state.data)
if (this.options.structuralSharing !== false) {
data = replaceEqualDeep(this.currentResult?.data, data)
}
} catch (selectError) {
getLogger().error(selectError)
error = selectError
errorUpdatedAt = Date.now()
status = 'error'
}
}
}
Expand Down Expand Up @@ -453,8 +466,8 @@ export class QueryObserver<
...getStatusProps(status),
data,
dataUpdatedAt,
error: state.error,
errorUpdatedAt: state.errorUpdatedAt,
error,
errorUpdatedAt,
failureCount: state.fetchFailureCount,
isFetched: state.dataUpdateCount > 0 || state.errorUpdateCount > 0,
isFetchedAfterMount:
Expand Down
44 changes: 39 additions & 5 deletions src/core/tests/queryObserver.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,30 @@ describe('queryObserver', () => {
select: select2,
})
await sleep(1)
await observer.refetch()
unsubscribe()
expect(count).toBe(2)
expect(results.length).toBe(2)
expect(results[0]).toMatchObject({ data: { myCount: 1 } })
expect(results[1]).toMatchObject({ data: { myCount: 99 } })
expect(results.length).toBe(4)
expect(results[0]).toMatchObject({
status: 'success',
isFetching: false,
data: { myCount: 1 },
})
expect(results[1]).toMatchObject({
status: 'success',
isFetching: false,
data: { myCount: 99 },
})
expect(results[2]).toMatchObject({
status: 'success',
isFetching: true,
data: { myCount: 99 },
})
expect(results[3]).toMatchObject({
status: 'success',
isFetching: false,
data: { myCount: 99 },
})
})

test('should not run the selector again if the data and selector did not change', async () => {
Expand All @@ -189,10 +208,25 @@ describe('queryObserver', () => {
select,
})
await sleep(1)
await observer.refetch()
unsubscribe()
expect(count).toBe(1)
expect(results.length).toBe(1)
expect(results[0]).toMatchObject({ data: { myCount: 1 } })
expect(results.length).toBe(3)
expect(results[0]).toMatchObject({
status: 'success',
isFetching: false,
data: { myCount: 1 },
})
expect(results[1]).toMatchObject({
status: 'success',
isFetching: true,
data: { myCount: 1 },
})
expect(results[2]).toMatchObject({
status: 'success',
isFetching: false,
data: { myCount: 1 },
})
})

test('should not run the selector again if the data did not change', async () => {
Expand Down
29 changes: 29 additions & 0 deletions src/react/tests/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,35 @@ describe('useQuery', () => {
expect(states[1]).toMatchObject({ data: 'test' })
})

it('should throw an error when a selector throws', async () => {
const consoleMock = mockConsoleError()
const key = queryKey()
const states: UseQueryResult<string>[] = []
const error = new Error('Select Error')

function Page() {
const state = useQuery(key, () => ({ name: 'test' }), {
select: () => {
throw error
},
})
states.push(state)
return null
}

renderWithClient(queryClient, <Page />)

await sleep(10)

expect(consoleMock).toHaveBeenCalledWith(error)
expect(states.length).toBe(2)

expect(states[0]).toMatchObject({ status: 'loading', data: undefined })
expect(states[1]).toMatchObject({ status: 'error', error })

consoleMock.mockRestore()
})

it('should re-render when dataUpdatedAt changes but data remains the same', async () => {
const key = queryKey()
const states: UseQueryResult<string>[] = []
Expand Down