Skip to content

Commit

Permalink
Merge remote-tracking branch 'react-query/master' into alpha
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/core/query.ts
#	src/core/queryObserver.ts
#	src/reactjs/tests/useQuery.test.tsx
#	src/reactjs/types.ts
  • Loading branch information
TkDodo committed Dec 28, 2021
2 parents 9a20452 + 93824ec commit 250c654
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 47 deletions.
2 changes: 1 addition & 1 deletion examples/playground/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ function fetchTodoById({ id }) {
new Error(JSON.stringify({ fetchTodoById: { id } }, null, 2))
);
}
resolve(list.find((d) => d.id == id));
resolve(list.find((d) => d.id === id));
}, queryTimeMin + Math.random() * (queryTimeMax - queryTimeMin));
});
}
Expand Down
5 changes: 4 additions & 1 deletion src/core/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,10 @@ export class Query<
fetchStatus: canFetch(this.options.networkMode)
? 'fetching'
: 'paused',
status: !state.dataUpdatedAt ? 'loading' : state.status,
...(!state.dataUpdatedAt && {
error: null,
status: 'loading',
}),
}
case 'success':
return {
Expand Down
14 changes: 10 additions & 4 deletions src/core/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ export class QueryObserver<
>
private previousQueryResult?: QueryObserverResult<TData, TError>
private previousSelectError: TError | null
private previousSelectFn?: (data: TQueryData) => TData
private previousSelect?: {
fn: (data: TQueryData) => TData
result: TData
}
private staleTimeoutId?: ReturnType<typeof setTimeout>
private refetchIntervalId?: ReturnType<typeof setInterval>
private currentRefetchInterval?: number | false
Expand Down Expand Up @@ -473,14 +476,17 @@ export class QueryObserver<
if (
prevResult &&
state.data === prevResultState?.data &&
options.select === this.previousSelectFn &&
options.select === this.previousSelect?.fn &&
!this.previousSelectError
) {
data = prevResult.data
data = this.previousSelect.result
} else {
try {
this.previousSelectFn = options.select
data = options.select(state.data)
this.previousSelect = {
fn: options.select,
result: data,
}
if (options.structuralSharing !== false) {
data = replaceEqualDeep(prevResult?.data, data)
}
Expand Down
1 change: 1 addition & 0 deletions src/devtools/styledComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const QueryKey = styled('span', {

export const Code = styled('code', {
fontSize: '.9em',
color: 'inherit',
})

export const Input = styled('input', (_props, theme) => ({
Expand Down
126 changes: 126 additions & 0 deletions src/reactjs/tests/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4102,6 +4102,56 @@ describe('useQuery', () => {
expect(selectRun).toBe(3)
})

it('select should always return the correct state', async () => {
const key1 = queryKey()

function Page() {
const [count, inc] = React.useReducer(prev => prev + 1, 2)
const [forceValue, forceUpdate] = React.useReducer(prev => prev + 1, 1)

const state = useQuery(
key1,
async () => {
await sleep(10)
return 0
},
{
select: React.useCallback(
(data: number) => {
return `selected ${data + count}`
},
[count]
),
placeholderData: 99,
}
)

return (
<div>
<h2>Data: {state.data}</h2>
<h2>forceValue: {forceValue}</h2>
<button onClick={inc}>inc: {count}</button>
<button onClick={forceUpdate}>forceUpdate</button>
</div>
)
}

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

await waitFor(() => rendered.getByText('Data: selected 2')) // 0 + 2

rendered.getByRole('button', { name: /inc/i }).click()

await waitFor(() => rendered.getByText('Data: selected 3')) // 0 + 3

rendered.getByRole('button', { name: /forceUpdate/i }).click()

await waitFor(() => rendered.getByText('forceValue: 2'))
// data should still be 3 after an independent re-render
await waitFor(() => rendered.getByText('Data: selected 3'))
})

it('should cancel the query function when there are no more subscriptions', async () => {
const key = queryKey()
let cancelFn: jest.Mock = jest.fn()
Expand Down Expand Up @@ -4611,6 +4661,82 @@ describe('useQuery', () => {
consoleMock.mockRestore()
})

it('should have no error in loading state when refetching after error occurred', async () => {
const consoleMock = mockConsoleError()
const key = queryKey()
const states: UseQueryResult<number>[] = []
const error = new Error('oops')

let count = 0

function Page() {
const state = useQuery(
key,
async () => {
await sleep(10)
if (count === 0) {
count++
throw error
}
return 5
},
{
retry: false,
}
)

states.push(state)

if (state.isLoading) {
return <div>status: loading</div>
}
if (state.error instanceof Error) {
return (
<div>
<div>error</div>
<button onClick={() => state.refetch()}>refetch</button>
</div>
)
}
return <div>data: {state.data}</div>
}

const rendered = renderWithClient(queryClient, <Page />)

await waitFor(() => rendered.getByText('error'))

fireEvent.click(rendered.getByRole('button', { name: 'refetch' }))
await waitFor(() => rendered.getByText('data: 5'))

await waitFor(() => expect(states.length).toBe(4))

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

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

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

expect(states[3]).toMatchObject({
status: 'success',
data: 5,
error: null,
})

consoleMock.mockRestore()
})

describe('networkMode online', () => {
it('online queries should not start fetching if you are offline', async () => {
const onlineMock = mockNavigatorOnLine(false)
Expand Down
49 changes: 8 additions & 41 deletions src/reactjs/types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { RetryValue, RetryDelayValue } from '../core/retryer'
import {
InfiniteQueryObserverOptions,
InfiniteQueryObserverResult,
MutationObserverResult,
MutationKey,
QueryObserverOptions,
QueryObserverResult,
QueryKey,
MutationFunction,
MutateOptions,
MutationMeta,
NetworkMode,
MutationObserverOptions,
MutateFunction,
} from '../core/types'

export interface UseBaseQueryOptions<
Expand Down Expand Up @@ -74,55 +70,26 @@ export interface UseMutationOptions<
TError = unknown,
TVariables = void,
TContext = unknown
> {
mutationFn?: MutationFunction<TData, TVariables>
mutationKey?: MutationKey
cacheTime?: number
onMutate?: (
variables: TVariables
) => Promise<TContext | undefined> | TContext | undefined
onSuccess?: (
data: TData,
variables: TVariables,
context: TContext | undefined
) => Promise<unknown> | void
onError?: (
error: TError,
variables: TVariables,
context: TContext | undefined
) => Promise<unknown> | void
onSettled?: (
data: TData | undefined,
error: TError | null,
variables: TVariables,
context: TContext | undefined
) => Promise<unknown> | void
retry?: RetryValue<TError>
retryDelay?: RetryDelayValue<TError>
networkMode?: NetworkMode
useErrorBoundary?: boolean | ((error: TError) => boolean)
meta?: MutationMeta
}
> extends Omit<
MutationObserverOptions<TData, TError, TVariables, TContext>,
'_defaulted' | 'variables'
> {}

export type UseMutateFunction<
TData = unknown,
TError = unknown,
TVariables = void,
TContext = unknown
> = (
variables: TVariables,
options?: MutateOptions<TData, TError, TVariables, TContext>
...args: Parameters<MutateFunction<TData, TError, TVariables, TContext>>
) => void

export type UseMutateAsyncFunction<
TData = unknown,
TError = unknown,
TVariables = void,
TContext = unknown
> = (
variables: TVariables,
options?: MutateOptions<TData, TError, TVariables, TContext>
) => Promise<TData>
> = MutateFunction<TData, TError, TVariables, TContext>

export type UseBaseMutationResult<
TData = unknown,
Expand Down

0 comments on commit 250c654

Please sign in to comment.