Skip to content

Commit 40b921a

Browse files
committed
Merge remote-tracking branch 'tannerlinsley/master' into beta
# Conflicts: # src/core/queryObserver.ts # src/core/tests/queryObserver.test.tsx # src/devtools/tests/devtools.test.tsx # src/reactjs/tests/useQuery.test.tsx
2 parents 892fc08 + 5848fab commit 40b921a

File tree

10 files changed

+229
-56
lines changed

10 files changed

+229
-56
lines changed

docs/src/pages/react-native.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,21 @@ onlineManager.setEventListener(setOnline => {
2828
## Refetch on App focus
2929

3030
In React Native you have to use React Query `focusManager` to refetch when the App is focused.
31-
You can use 'react-native-appstate-hook' to be notified when the App state has changed.
3231

3332
```ts
3433
import { focusManager } from 'react-query'
35-
import useAppState from 'react-native-appstate-hook'
3634

3735
function onAppStateChange(status: AppStateStatus) {
3836
if (Platform.OS !== 'web') {
3937
focusManager.setFocused(status === 'active')
4038
}
4139
}
4240

43-
useAppState({
44-
onChange: onAppStateChange,
45-
})
41+
useEffect(() => {
42+
const subscription = AppState.addEventListener('change', onAppStateChange)
43+
44+
return () => subscription.remove()
45+
}, [])
4646
```
4747

4848
## Refresh on Screen focus

docs/src/pages/reference/useQuery.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ const result = useQuery({
244244
- The failure count for the query.
245245
- Incremented every time the query fails.
246246
- Reset to `0` when the query succeeds.
247+
- `errorUpdateCount: number`
248+
- The sum of all errors.
247249
- `refetch: (options: { throwOnError: boolean, cancelRefetch: boolean }) => Promise<UseQueryResult>`
248250
- A function to manually refetch the query.
249251
- If the query errors, the error will only be logged. If you want an error to be thrown, pass the `throwOnError: true` option

src/core/queryObserver.ts

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,9 @@ export class QueryObserver<
6666
TQueryKey
6767
>
6868
private previousQueryResult?: QueryObserverResult<TData, TError>
69-
private previousSelectError: TError | null
70-
private previousSelect?: {
71-
fn: (data: TQueryData) => TData
72-
result: TData
73-
}
69+
private selectError: TError | null
70+
private selectFn?: (data: TQueryData) => TData
71+
private selectResult?: TData
7472
private staleTimeoutId?: ReturnType<typeof setTimeout>
7573
private refetchIntervalId?: ReturnType<typeof setInterval>
7674
private currentRefetchInterval?: number | false
@@ -91,7 +89,7 @@ export class QueryObserver<
9189
this.client = client
9290
this.options = options
9391
this.trackedProps = new Set()
94-
this.previousSelectError = null
92+
this.selectError = null
9593
this.bindMethods()
9694
this.setOptions(options)
9795
}
@@ -462,29 +460,23 @@ export class QueryObserver<
462460
if (
463461
prevResult &&
464462
state.data === prevResultState?.data &&
465-
options.select === this.previousSelect?.fn &&
466-
!this.previousSelectError
463+
options.select === this.selectFn
467464
) {
468-
data = this.previousSelect.result
465+
data = this.selectResult
469466
} else {
470467
try {
468+
this.selectFn = options.select
471469
data = options.select(state.data)
472470
if (options.structuralSharing !== false) {
473471
data = replaceEqualDeep(prevResult?.data, data)
474472
}
475-
this.previousSelect = {
476-
fn: options.select,
477-
result: data,
478-
}
479-
this.previousSelectError = null
473+
this.selectResult = data
474+
this.selectError = null
480475
} catch (selectError) {
481476
if (process.env.NODE_ENV !== 'production') {
482477
this.client.getLogger().error(selectError)
483478
}
484-
error = selectError as TError
485-
this.previousSelectError = selectError as TError
486-
errorUpdatedAt = Date.now()
487-
status = 'error'
479+
this.selectError = selectError as TError
488480
}
489481
}
490482
}
@@ -521,15 +513,12 @@ export class QueryObserver<
521513
placeholderData
522514
)
523515
}
524-
this.previousSelectError = null
516+
this.selectError = null
525517
} catch (selectError) {
526518
if (process.env.NODE_ENV !== 'production') {
527519
this.client.getLogger().error(selectError)
528520
}
529-
error = selectError as TError
530-
this.previousSelectError = selectError as TError
531-
errorUpdatedAt = Date.now()
532-
status = 'error'
521+
this.selectError = selectError as TError
533522
}
534523
}
535524
}
@@ -541,6 +530,13 @@ export class QueryObserver<
541530
}
542531
}
543532

533+
if (this.selectError) {
534+
error = this.selectError as any
535+
data = this.selectResult
536+
errorUpdatedAt = Date.now()
537+
status = 'error'
538+
}
539+
544540
const isFetching = fetchStatus === 'fetching'
545541

546542
const result: QueryObserverBaseResult<TData, TError> = {
@@ -554,6 +550,7 @@ export class QueryObserver<
554550
error,
555551
errorUpdatedAt,
556552
failureCount: state.fetchFailureCount,
553+
errorUpdateCount: state.errorUpdateCount,
557554
isFetched: state.dataUpdateCount > 0 || state.errorUpdateCount > 0,
558555
isFetchedAfterMount:
559556
state.dataUpdateCount > queryInitialState.dataUpdateCount ||

src/core/tests/queryObserver.test.tsx

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -246,25 +246,24 @@ describe('queryObserver', () => {
246246
expect(observerResult2.data).toMatchObject({ myCount: 1 })
247247
})
248248

249-
test('should always run the selector again if selector throws an error', async () => {
249+
test('should always run the selector again if selector throws an error and selector is not referentially stable', async () => {
250250
const key = queryKey()
251251
const results: QueryObserverResult[] = []
252-
const select = () => {
253-
throw new Error('selector error')
254-
}
255252
const queryFn = () => ({ count: 1 })
256253
const observer = new QueryObserver(queryClient, {
257254
queryKey: key,
258255
queryFn,
259-
select,
256+
select: () => {
257+
throw new Error('selector error')
258+
},
260259
})
261260
const unsubscribe = observer.subscribe(result => {
262261
results.push(result)
263262
})
264263
await sleep(1)
265264
await observer.refetch()
266265
unsubscribe()
267-
expect(results.length).toBe(5)
266+
expect(results.length).toBe(4)
268267
expect(results[0]).toMatchObject({
269268
status: 'loading',
270269
isFetching: true,
@@ -285,10 +284,56 @@ describe('queryObserver', () => {
285284
isFetching: false,
286285
data: undefined,
287286
})
288-
expect(results[4]).toMatchObject({
287+
})
288+
289+
test('should return stale data if selector throws an error', async () => {
290+
const key = queryKey()
291+
const results: QueryObserverResult[] = []
292+
let shouldError = false
293+
const error = new Error('select error')
294+
const observer = new QueryObserver(queryClient, {
295+
queryKey: key,
296+
queryFn: () => (shouldError ? 2 : 1),
297+
select: num => {
298+
if (shouldError) {
299+
throw error
300+
}
301+
shouldError = true
302+
return String(num)
303+
},
304+
})
305+
306+
const unsubscribe = observer.subscribe(result => {
307+
results.push(result)
308+
})
309+
await sleep(10)
310+
await observer.refetch()
311+
unsubscribe()
312+
313+
expect(results.length).toBe(4)
314+
expect(results[0]).toMatchObject({
315+
status: 'loading',
316+
isFetching: true,
317+
data: undefined,
318+
error: null,
319+
})
320+
expect(results[1]).toMatchObject({
321+
status: 'success',
322+
isFetching: false,
323+
data: '1',
324+
error: null,
325+
})
326+
expect(results[2]).toMatchObject({
327+
status: 'success',
328+
isFetching: true,
329+
data: '1',
330+
error: null,
331+
})
332+
expect(results[3]).toMatchObject({
289333
status: 'error',
290334
isFetching: false,
291-
data: undefined,
335+
data: '1',
336+
error,
292337
})
293338
})
294339

src/core/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ export interface QueryObserverBaseResult<TData = unknown, TError = unknown> {
357357
error: TError | null
358358
errorUpdatedAt: number
359359
failureCount: number
360+
errorUpdateCount: number
360361
isError: boolean
361362
isFetched: boolean
362363
isFetchedAfterMount: boolean

src/devtools/Explorer.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ export const LabelButton = styled('button', {
1919
color: 'white',
2020
})
2121

22+
export const ExpandButton = styled('button', {
23+
cursor: 'pointer',
24+
color: 'inherit',
25+
font: 'inherit',
26+
outline: 'inherit',
27+
background: 'transparent',
28+
border: 'none',
29+
padding: 0,
30+
})
31+
2232
export const Value = styled('span', (_props, theme) => ({
2333
color: theme.danger,
2434
}))
@@ -107,13 +117,13 @@ export const DefaultRenderer: Renderer = ({
107117
<Entry key={label}>
108118
{subEntryPages.length ? (
109119
<>
110-
<button onClick={() => toggleExpanded()}>
120+
<ExpandButton onClick={() => toggleExpanded()}>
111121
<Expander expanded={expanded} /> {label}{' '}
112122
<Info>
113123
{String(type).toLowerCase() === 'iterable' ? '(Iterable) ' : ''}
114124
{subEntries.length} {subEntries.length > 1 ? `items` : `item`}
115125
</Info>
116-
</button>
126+
</ExpandButton>
117127
{expanded ? (
118128
subEntryPages.length === 1 ? (
119129
<SubEntries>

src/devtools/devtools.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,6 @@ export function ReactQueryDevtools({
289289
{isResolvedOpen ? (
290290
<Button
291291
type="button"
292-
aria-label="Close React Query Devtools"
293292
aria-controls="ReactQueryDevtoolsPanel"
294293
aria-haspopup="true"
295294
aria-expanded="true"
@@ -545,12 +544,24 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
545544
alignItems: 'center',
546545
}}
547546
>
548-
<Logo
549-
aria-hidden
547+
<button
548+
type="button"
549+
aria-label="Close React Query Devtools"
550+
aria-controls="ReactQueryDevtoolsPanel"
551+
aria-haspopup="true"
552+
aria-expanded="true"
553+
onClick={() => setIsOpen(false)}
550554
style={{
555+
display: 'inline-flex',
556+
background: 'none',
557+
border: 0,
558+
padding: 0,
551559
marginRight: '.5em',
560+
cursor: 'pointer',
552561
}}
553-
/>
562+
>
563+
<Logo aria-hidden />
564+
</button>
554565
<div
555566
style={{
556567
display: 'flex',
@@ -575,6 +586,7 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
575586
style={{
576587
flex: '1',
577588
marginRight: '.5em',
589+
width: '100%',
578590
}}
579591
/>
580592
{!filter ? (

src/devtools/tests/devtools.test.tsx

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import React from 'react'
2-
32
import { fireEvent, screen, waitFor, act } from '@testing-library/react'
43
import { ErrorBoundary } from 'react-error-boundary'
5-
64
import '@testing-library/jest-dom'
75
import { useQuery, QueryClient } from '../..'
86
import {
@@ -54,23 +52,56 @@ describe('ReactQueryDevtools', () => {
5452
toggleButtonProps: { onClick: onToggleClick },
5553
})
5654

57-
const closeButton = screen.queryByRole('button', {
58-
name: /close react query devtools/i,
59-
})
60-
expect(closeButton).toBeNull()
61-
fireEvent.click(
62-
screen.getByRole('button', { name: /open react query devtools/i })
63-
)
55+
const verifyDevtoolsIsOpen = () => {
56+
expect(
57+
screen.queryByRole('generic', { name: /react query devtools panel/i })
58+
).not.toBeNull()
59+
expect(
60+
screen.queryByRole('button', { name: /open react query devtools/i })
61+
).toBeNull()
62+
}
63+
const verifyDevtoolsIsClosed = () => {
64+
expect(
65+
screen.queryByRole('generic', { name: /react query devtools panel/i })
66+
).toBeNull()
67+
expect(
68+
screen.queryByRole('button', { name: /open react query devtools/i })
69+
).not.toBeNull()
70+
}
6471

65-
expect(onToggleClick).toHaveBeenCalledTimes(1)
72+
const waitForDevtoolsToOpen = () =>
73+
screen.findByRole('button', { name: /close react query devtools/i })
74+
const waitForDevtoolsToClose = () =>
75+
screen.findByRole('button', { name: /open react query devtools/i })
6676

67-
fireEvent.click(
77+
const getOpenLogoButton = () =>
78+
screen.getByRole('button', { name: /open react query devtools/i })
79+
const getCloseLogoButton = () =>
6880
screen.getByRole('button', { name: /close react query devtools/i })
69-
)
81+
const getCloseButton = () =>
82+
screen.getByRole('button', { name: /^close$/i })
83+
84+
verifyDevtoolsIsClosed()
85+
86+
fireEvent.click(getOpenLogoButton())
87+
await waitForDevtoolsToOpen()
88+
89+
verifyDevtoolsIsOpen()
90+
91+
fireEvent.click(getCloseLogoButton())
92+
await waitForDevtoolsToClose()
93+
94+
verifyDevtoolsIsClosed()
95+
96+
fireEvent.click(getOpenLogoButton())
97+
await waitForDevtoolsToOpen()
98+
99+
verifyDevtoolsIsOpen()
70100

71-
await screen.findByRole('button', { name: /open react query devtools/i })
101+
fireEvent.click(getCloseButton())
102+
await waitForDevtoolsToClose()
72103

73-
expect(onCloseClick).toHaveBeenCalledTimes(1)
104+
verifyDevtoolsIsClosed()
74105
})
75106

76107
it('should be able to drag devtools without error', async () => {

0 commit comments

Comments
 (0)