Skip to content

Commit c921585

Browse files
authored
fix: handle select errors (#1760)
1 parent 6182360 commit c921585

File tree

3 files changed

+86
-10
lines changed

3 files changed

+86
-10
lines changed

src/core/queryObserver.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type { Query, QueryState, Action, FetchOptions } from './query'
2121
import type { QueryClient } from './queryClient'
2222
import { focusManager } from './focusManager'
2323
import { Subscribable } from './subscribable'
24+
import { getLogger } from './logger'
2425

2526
type QueryObserverListener<TData, TError> = (
2627
result: QueryObserverResult<TData, TError>
@@ -240,6 +241,9 @@ export class QueryObserver<
240241
this.updateRefetchInterval()
241242
}
242243
}
244+
245+
// Reset previous options after all code related to option changes has run
246+
this.previousOptions = this.options
243247
}
244248

245249
getCurrentResult(): QueryObserverResult<TData, TError> {
@@ -390,6 +394,8 @@ export class QueryObserver<
390394
let isPlaceholderData = false
391395
let data: TData | undefined
392396
let dataUpdatedAt = state.dataUpdatedAt
397+
let error = state.error
398+
let errorUpdatedAt = state.errorUpdatedAt
393399

394400
// Optimistically set status to loading if we will start fetching
395401
if (!this.hasListeners() && this.willFetchOnMount()) {
@@ -421,9 +427,16 @@ export class QueryObserver<
421427
) {
422428
data = this.currentResult.data
423429
} else {
424-
data = this.options.select(state.data)
425-
if (this.options.structuralSharing !== false) {
426-
data = replaceEqualDeep(this.currentResult?.data, data)
430+
try {
431+
data = this.options.select(state.data)
432+
if (this.options.structuralSharing !== false) {
433+
data = replaceEqualDeep(this.currentResult?.data, data)
434+
}
435+
} catch (selectError) {
436+
getLogger().error(selectError)
437+
error = selectError
438+
errorUpdatedAt = Date.now()
439+
status = 'error'
427440
}
428441
}
429442
}
@@ -453,8 +466,8 @@ export class QueryObserver<
453466
...getStatusProps(status),
454467
data,
455468
dataUpdatedAt,
456-
error: state.error,
457-
errorUpdatedAt: state.errorUpdatedAt,
469+
error,
470+
errorUpdatedAt,
458471
failureCount: state.fetchFailureCount,
459472
isFetched: state.dataUpdateCount > 0 || state.errorUpdateCount > 0,
460473
isFetchedAfterMount:

src/core/tests/queryObserver.test.tsx

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,11 +158,30 @@ describe('queryObserver', () => {
158158
select: select2,
159159
})
160160
await sleep(1)
161+
await observer.refetch()
161162
unsubscribe()
162163
expect(count).toBe(2)
163-
expect(results.length).toBe(2)
164-
expect(results[0]).toMatchObject({ data: { myCount: 1 } })
165-
expect(results[1]).toMatchObject({ data: { myCount: 99 } })
164+
expect(results.length).toBe(4)
165+
expect(results[0]).toMatchObject({
166+
status: 'success',
167+
isFetching: false,
168+
data: { myCount: 1 },
169+
})
170+
expect(results[1]).toMatchObject({
171+
status: 'success',
172+
isFetching: false,
173+
data: { myCount: 99 },
174+
})
175+
expect(results[2]).toMatchObject({
176+
status: 'success',
177+
isFetching: true,
178+
data: { myCount: 99 },
179+
})
180+
expect(results[3]).toMatchObject({
181+
status: 'success',
182+
isFetching: false,
183+
data: { myCount: 99 },
184+
})
166185
})
167186

168187
test('should not run the selector again if the data and selector did not change', async () => {
@@ -189,10 +208,25 @@ describe('queryObserver', () => {
189208
select,
190209
})
191210
await sleep(1)
211+
await observer.refetch()
192212
unsubscribe()
193213
expect(count).toBe(1)
194-
expect(results.length).toBe(1)
195-
expect(results[0]).toMatchObject({ data: { myCount: 1 } })
214+
expect(results.length).toBe(3)
215+
expect(results[0]).toMatchObject({
216+
status: 'success',
217+
isFetching: false,
218+
data: { myCount: 1 },
219+
})
220+
expect(results[1]).toMatchObject({
221+
status: 'success',
222+
isFetching: true,
223+
data: { myCount: 1 },
224+
})
225+
expect(results[2]).toMatchObject({
226+
status: 'success',
227+
isFetching: false,
228+
data: { myCount: 1 },
229+
})
196230
})
197231

198232
test('should not run the selector again if the data did not change', async () => {

src/react/tests/useQuery.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,35 @@ describe('useQuery', () => {
729729
expect(states[1]).toMatchObject({ data: 'test' })
730730
})
731731

732+
it('should throw an error when a selector throws', async () => {
733+
const consoleMock = mockConsoleError()
734+
const key = queryKey()
735+
const states: UseQueryResult<string>[] = []
736+
const error = new Error('Select Error')
737+
738+
function Page() {
739+
const state = useQuery(key, () => ({ name: 'test' }), {
740+
select: () => {
741+
throw error
742+
},
743+
})
744+
states.push(state)
745+
return null
746+
}
747+
748+
renderWithClient(queryClient, <Page />)
749+
750+
await sleep(10)
751+
752+
expect(consoleMock).toHaveBeenCalledWith(error)
753+
expect(states.length).toBe(2)
754+
755+
expect(states[0]).toMatchObject({ status: 'loading', data: undefined })
756+
expect(states[1]).toMatchObject({ status: 'error', error })
757+
758+
consoleMock.mockRestore()
759+
})
760+
732761
it('should re-render when dataUpdatedAt changes but data remains the same', async () => {
733762
const key = queryKey()
734763
const states: UseQueryResult<string>[] = []

0 commit comments

Comments
 (0)