Skip to content

Commit

Permalink
Merge remote-tracking branch 'tannerlinsley/master' into beta
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/core/queryObserver.ts
#	src/core/tests/queryObserver.test.tsx
#	src/devtools/tests/devtools.test.tsx
#	src/reactjs/tests/useQuery.test.tsx
TkDodo committed May 25, 2022
2 parents 892fc08 + 5848fab commit 40b921a
Showing 10 changed files with 229 additions and 56 deletions.
10 changes: 5 additions & 5 deletions docs/src/pages/react-native.md
Original file line number Diff line number Diff line change
@@ -28,21 +28,21 @@ onlineManager.setEventListener(setOnline => {
## Refetch on App focus

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

```ts
import { focusManager } from 'react-query'
import useAppState from 'react-native-appstate-hook'

function onAppStateChange(status: AppStateStatus) {
if (Platform.OS !== 'web') {
focusManager.setFocused(status === 'active')
}
}

useAppState({
onChange: onAppStateChange,
})
useEffect(() => {
const subscription = AppState.addEventListener('change', onAppStateChange)

return () => subscription.remove()
}, [])
```

## Refresh on Screen focus
2 changes: 2 additions & 0 deletions docs/src/pages/reference/useQuery.md
Original file line number Diff line number Diff line change
@@ -244,6 +244,8 @@ const result = useQuery({
- The failure count for the query.
- Incremented every time the query fails.
- Reset to `0` when the query succeeds.
- `errorUpdateCount: number`
- The sum of all errors.
- `refetch: (options: { throwOnError: boolean, cancelRefetch: boolean }) => Promise<UseQueryResult>`
- A function to manually refetch the query.
- If the query errors, the error will only be logged. If you want an error to be thrown, pass the `throwOnError: true` option
43 changes: 20 additions & 23 deletions src/core/queryObserver.ts
Original file line number Diff line number Diff line change
@@ -66,11 +66,9 @@ export class QueryObserver<
TQueryKey
>
private previousQueryResult?: QueryObserverResult<TData, TError>
private previousSelectError: TError | null
private previousSelect?: {
fn: (data: TQueryData) => TData
result: TData
}
private selectError: TError | null
private selectFn?: (data: TQueryData) => TData
private selectResult?: TData
private staleTimeoutId?: ReturnType<typeof setTimeout>
private refetchIntervalId?: ReturnType<typeof setInterval>
private currentRefetchInterval?: number | false
@@ -91,7 +89,7 @@ export class QueryObserver<
this.client = client
this.options = options
this.trackedProps = new Set()
this.previousSelectError = null
this.selectError = null
this.bindMethods()
this.setOptions(options)
}
@@ -462,29 +460,23 @@ export class QueryObserver<
if (
prevResult &&
state.data === prevResultState?.data &&
options.select === this.previousSelect?.fn &&
!this.previousSelectError
options.select === this.selectFn
) {
data = this.previousSelect.result
data = this.selectResult
} else {
try {
this.selectFn = options.select
data = options.select(state.data)
if (options.structuralSharing !== false) {
data = replaceEqualDeep(prevResult?.data, data)
}
this.previousSelect = {
fn: options.select,
result: data,
}
this.previousSelectError = null
this.selectResult = data
this.selectError = null
} catch (selectError) {
if (process.env.NODE_ENV !== 'production') {
this.client.getLogger().error(selectError)
}
error = selectError as TError
this.previousSelectError = selectError as TError
errorUpdatedAt = Date.now()
status = 'error'
this.selectError = selectError as TError
}
}
}
@@ -521,15 +513,12 @@ export class QueryObserver<
placeholderData
)
}
this.previousSelectError = null
this.selectError = null
} catch (selectError) {
if (process.env.NODE_ENV !== 'production') {
this.client.getLogger().error(selectError)
}
error = selectError as TError
this.previousSelectError = selectError as TError
errorUpdatedAt = Date.now()
status = 'error'
this.selectError = selectError as TError
}
}
}
@@ -541,6 +530,13 @@ export class QueryObserver<
}
}

if (this.selectError) {
error = this.selectError as any
data = this.selectResult
errorUpdatedAt = Date.now()
status = 'error'
}

const isFetching = fetchStatus === 'fetching'

const result: QueryObserverBaseResult<TData, TError> = {
@@ -554,6 +550,7 @@ export class QueryObserver<
error,
errorUpdatedAt,
failureCount: state.fetchFailureCount,
errorUpdateCount: state.errorUpdateCount,
isFetched: state.dataUpdateCount > 0 || state.errorUpdateCount > 0,
isFetchedAfterMount:
state.dataUpdateCount > queryInitialState.dataUpdateCount ||
61 changes: 53 additions & 8 deletions src/core/tests/queryObserver.test.tsx
Original file line number Diff line number Diff line change
@@ -246,25 +246,24 @@ describe('queryObserver', () => {
expect(observerResult2.data).toMatchObject({ myCount: 1 })
})

test('should always run the selector again if selector throws an error', async () => {
test('should always run the selector again if selector throws an error and selector is not referentially stable', async () => {
const key = queryKey()
const results: QueryObserverResult[] = []
const select = () => {
throw new Error('selector error')
}
const queryFn = () => ({ count: 1 })
const observer = new QueryObserver(queryClient, {
queryKey: key,
queryFn,
select,
select: () => {
throw new Error('selector error')
},
})
const unsubscribe = observer.subscribe(result => {
results.push(result)
})
await sleep(1)
await observer.refetch()
unsubscribe()
expect(results.length).toBe(5)
expect(results.length).toBe(4)
expect(results[0]).toMatchObject({
status: 'loading',
isFetching: true,
@@ -285,10 +284,56 @@ describe('queryObserver', () => {
isFetching: false,
data: undefined,
})
expect(results[4]).toMatchObject({
})

test('should return stale data if selector throws an error', async () => {
const key = queryKey()
const results: QueryObserverResult[] = []
let shouldError = false
const error = new Error('select error')
const observer = new QueryObserver(queryClient, {
queryKey: key,
queryFn: () => (shouldError ? 2 : 1),
select: num => {
if (shouldError) {
throw error
}
shouldError = true
return String(num)
},
})

const unsubscribe = observer.subscribe(result => {
results.push(result)
})
await sleep(10)
await observer.refetch()
unsubscribe()

expect(results.length).toBe(4)
expect(results[0]).toMatchObject({
status: 'loading',
isFetching: true,
data: undefined,
error: null,
})
expect(results[1]).toMatchObject({
status: 'success',
isFetching: false,
data: '1',
error: null,
})
expect(results[2]).toMatchObject({
status: 'success',
isFetching: true,
data: '1',
error: null,
})
expect(results[3]).toMatchObject({
status: 'error',
isFetching: false,
data: undefined,
data: '1',
error,
})
})

1 change: 1 addition & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
@@ -357,6 +357,7 @@ export interface QueryObserverBaseResult<TData = unknown, TError = unknown> {
error: TError | null
errorUpdatedAt: number
failureCount: number
errorUpdateCount: number
isError: boolean
isFetched: boolean
isFetchedAfterMount: boolean
14 changes: 12 additions & 2 deletions src/devtools/Explorer.tsx
Original file line number Diff line number Diff line change
@@ -19,6 +19,16 @@ export const LabelButton = styled('button', {
color: 'white',
})

export const ExpandButton = styled('button', {
cursor: 'pointer',
color: 'inherit',
font: 'inherit',
outline: 'inherit',
background: 'transparent',
border: 'none',
padding: 0,
})

export const Value = styled('span', (_props, theme) => ({
color: theme.danger,
}))
@@ -107,13 +117,13 @@ export const DefaultRenderer: Renderer = ({
<Entry key={label}>
{subEntryPages.length ? (
<>
<button onClick={() => toggleExpanded()}>
<ExpandButton onClick={() => toggleExpanded()}>
<Expander expanded={expanded} /> {label}{' '}
<Info>
{String(type).toLowerCase() === 'iterable' ? '(Iterable) ' : ''}
{subEntries.length} {subEntries.length > 1 ? `items` : `item`}
</Info>
</button>
</ExpandButton>
{expanded ? (
subEntryPages.length === 1 ? (
<SubEntries>
20 changes: 16 additions & 4 deletions src/devtools/devtools.tsx
Original file line number Diff line number Diff line change
@@ -289,7 +289,6 @@ export function ReactQueryDevtools({
{isResolvedOpen ? (
<Button
type="button"
aria-label="Close React Query Devtools"
aria-controls="ReactQueryDevtoolsPanel"
aria-haspopup="true"
aria-expanded="true"
@@ -545,12 +544,24 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
alignItems: 'center',
}}
>
<Logo
aria-hidden
<button
type="button"
aria-label="Close React Query Devtools"
aria-controls="ReactQueryDevtoolsPanel"
aria-haspopup="true"
aria-expanded="true"
onClick={() => setIsOpen(false)}
style={{
display: 'inline-flex',
background: 'none',
border: 0,
padding: 0,
marginRight: '.5em',
cursor: 'pointer',
}}
/>
>
<Logo aria-hidden />
</button>
<div
style={{
display: 'flex',
@@ -575,6 +586,7 @@ export const ReactQueryDevtoolsPanel = React.forwardRef<
style={{
flex: '1',
marginRight: '.5em',
width: '100%',
}}
/>
{!filter ? (
59 changes: 45 additions & 14 deletions src/devtools/tests/devtools.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import React from 'react'

import { fireEvent, screen, waitFor, act } from '@testing-library/react'
import { ErrorBoundary } from 'react-error-boundary'

import '@testing-library/jest-dom'
import { useQuery, QueryClient } from '../..'
import {
@@ -54,23 +52,56 @@ describe('ReactQueryDevtools', () => {
toggleButtonProps: { onClick: onToggleClick },
})

const closeButton = screen.queryByRole('button', {
name: /close react query devtools/i,
})
expect(closeButton).toBeNull()
fireEvent.click(
screen.getByRole('button', { name: /open react query devtools/i })
)
const verifyDevtoolsIsOpen = () => {
expect(
screen.queryByRole('generic', { name: /react query devtools panel/i })
).not.toBeNull()
expect(
screen.queryByRole('button', { name: /open react query devtools/i })
).toBeNull()
}
const verifyDevtoolsIsClosed = () => {
expect(
screen.queryByRole('generic', { name: /react query devtools panel/i })
).toBeNull()
expect(
screen.queryByRole('button', { name: /open react query devtools/i })
).not.toBeNull()
}

expect(onToggleClick).toHaveBeenCalledTimes(1)
const waitForDevtoolsToOpen = () =>
screen.findByRole('button', { name: /close react query devtools/i })
const waitForDevtoolsToClose = () =>
screen.findByRole('button', { name: /open react query devtools/i })

fireEvent.click(
const getOpenLogoButton = () =>
screen.getByRole('button', { name: /open react query devtools/i })
const getCloseLogoButton = () =>
screen.getByRole('button', { name: /close react query devtools/i })
)
const getCloseButton = () =>
screen.getByRole('button', { name: /^close$/i })

verifyDevtoolsIsClosed()

fireEvent.click(getOpenLogoButton())
await waitForDevtoolsToOpen()

verifyDevtoolsIsOpen()

fireEvent.click(getCloseLogoButton())
await waitForDevtoolsToClose()

verifyDevtoolsIsClosed()

fireEvent.click(getOpenLogoButton())
await waitForDevtoolsToOpen()

verifyDevtoolsIsOpen()

await screen.findByRole('button', { name: /open react query devtools/i })
fireEvent.click(getCloseButton())
await waitForDevtoolsToClose()

expect(onCloseClick).toHaveBeenCalledTimes(1)
verifyDevtoolsIsClosed()
})

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

0 comments on commit 40b921a

Please sign in to comment.