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
6 changes: 1 addition & 5 deletions src/core/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,7 @@ export class Mutation<
removeObserver(observer: MutationObserver<any, any, any, any>): void {
this.observers = this.observers.filter(x => x !== observer)

if (this.cacheTime) {
this.scheduleGc()
} else {
this.optionalRemove()
}
this.scheduleGc()

this.mutationCache.notify({
type: 'observerRemoved',
Expand Down
33 changes: 12 additions & 21 deletions src/core/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,20 +154,19 @@ export class Query<
revertState?: QueryState<TData, TError>
state: QueryState<TData, TError>
meta: QueryMeta | undefined
isFetchingOptimistic?: boolean

private cache: QueryCache
private promise?: Promise<TData>
private retryer?: Retryer<TData, TError>
private observers: QueryObserver<any, any, any, any, any>[]
private defaultOptions?: QueryOptions<TQueryFnData, TError, TData, TQueryKey>
private abortSignalConsumed: boolean
private hadObservers: boolean

constructor(config: QueryConfig<TQueryFnData, TError, TData, TQueryKey>) {
super()

this.abortSignalConsumed = false
this.hadObservers = false
this.defaultOptions = config.defaultOptions
this.setOptions(config.options)
this.observers = []
Expand All @@ -177,7 +176,6 @@ export class Query<
this.initialState = config.state || this.getDefaultState(this.options)
this.state = this.initialState
this.meta = config.meta
this.scheduleGc()
}

private setOptions(
Expand All @@ -197,12 +195,8 @@ export class Query<
}

protected optionalRemove() {
if (!this.observers.length) {
if (this.state.fetchStatus === 'idle') {
this.cache.remove(this)
} else if (this.hadObservers) {
this.scheduleGc()
}
if (!this.observers.length && this.state.fetchStatus === 'idle') {
this.cache.remove(this)
}
}

Expand Down Expand Up @@ -307,7 +301,6 @@ export class Query<
addObserver(observer: QueryObserver<any, any, any, any, any>): void {
if (this.observers.indexOf(observer) === -1) {
this.observers.push(observer)
this.hadObservers = true

// Stop the query from being garbage collected
this.clearGcTimeout()
Expand All @@ -331,11 +324,7 @@ export class Query<
}
}

if (this.cacheTime) {
this.scheduleGc()
} else {
this.cache.remove(this)
}
this.scheduleGc()
}

this.cache.notify({ type: 'observerRemoved', query: this, observer })
Expand Down Expand Up @@ -455,10 +444,11 @@ export class Query<
// Notify cache callback
this.cache.config.onSuccess?.(data, this as Query<any, any, any, any>)

// Remove query after fetching if cache time is 0
if (this.cacheTime === 0) {
this.optionalRemove()
if (!this.isFetchingOptimistic) {
// Schedule query gc after fetching
this.scheduleGc()
}
this.isFetchingOptimistic = false
},
onError: (error: TError | { silent?: boolean }) => {
// Optimistically update state if needed
Expand All @@ -477,10 +467,11 @@ export class Query<
getLogger().error(error)
}

// Remove query after fetching if cache time is 0
if (this.cacheTime === 0) {
this.optionalRemove()
if (!this.isFetchingOptimistic) {
// Schedule query gc after fetching
this.scheduleGc()
}
this.isFetchingOptimistic = false
},
onFail: () => {
this.dispatch({ type: 'failed' })
Expand Down
23 changes: 3 additions & 20 deletions src/core/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,15 +309,8 @@ export class QueryObserver<

const query = this.client
.getQueryCache()
.build(
this.client,
defaultedOptions as QueryOptions<
TQueryFnData,
TError,
TQueryData,
TQueryKey
>
)
.build(this.client, defaultedOptions)
query.isFetchingOptimistic = true

return query.fetch().then(() => this.createResult(query, defaultedOptions))
}
Expand Down Expand Up @@ -655,17 +648,7 @@ export class QueryObserver<
}

private updateQuery(): void {
const query = this.client
.getQueryCache()
.build(
this.client,
this.options as QueryOptions<
TQueryFnData,
TError,
TQueryData,
TQueryKey
>
)
const query = this.client.getQueryCache().build(this.client, this.options)

if (query === this.currentQuery) {
return
Expand Down
15 changes: 7 additions & 8 deletions src/core/tests/query.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
onlineManager,
QueryFunctionContext,
} from '../..'
import { waitFor } from '@testing-library/react'

describe('query', () => {
let queryClient: QueryClient
Expand Down Expand Up @@ -472,7 +473,6 @@ describe('query', () => {
})

test('queries with cacheTime 0 should be removed immediately after unsubscribing', async () => {
const consoleMock = mockConsoleError()
const key = queryKey()
let count = 0
const observer = new QueryObserver(queryClient, {
Expand All @@ -486,13 +486,12 @@ describe('query', () => {
})
const unsubscribe1 = observer.subscribe()
unsubscribe1()
await sleep(10)
await waitFor(() => expect(queryCache.find(key)).toBeUndefined())
const unsubscribe2 = observer.subscribe()
unsubscribe2()
await sleep(10)
expect(count).toBe(2)
expect(queryCache.find(key)).toBeUndefined()
consoleMock.mockRestore()

await waitFor(() => expect(queryCache.find(key)).toBeUndefined())
expect(count).toBe(1)
})

test('should be garbage collected when unsubscribed to', async () => {
Expand All @@ -506,7 +505,7 @@ describe('query', () => {
const unsubscribe = observer.subscribe()
expect(queryCache.find(key)).toBeDefined()
unsubscribe()
expect(queryCache.find(key)).toBeUndefined()
await waitFor(() => expect(queryCache.find(key)).toBeUndefined())
})

test('should be garbage collected later when unsubscribed and query is fetching', async () => {
Expand All @@ -529,7 +528,7 @@ describe('query', () => {
expect(queryCache.find(key)).toBeDefined()
await sleep(10)
// should be removed after an additional staleTime wait
expect(queryCache.find(key)).toBeUndefined()
await waitFor(() => expect(queryCache.find(key)).toBeUndefined())
})

test('should not be garbage collected unless there are no subscribers', async () => {
Expand Down
5 changes: 3 additions & 2 deletions src/core/tests/queryClient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -416,9 +416,10 @@ describe('queryClient', () => {
},
{ cacheTime: 0 }
)
const result2 = queryClient.getQueryData(key1)
expect(result).toEqual(1)
expect(result2).toEqual(undefined)
await waitFor(() =>
expect(queryClient.getQueryData(key1)).toEqual(undefined)
)
})

test('should keep a query in cache if cache time is Infinity', async () => {
Expand Down
128 changes: 48 additions & 80 deletions src/reactjs/tests/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -691,52 +691,78 @@ describe('useQuery', () => {
expect(states[1]).toMatchObject({ data: 'data' })
})

it('should create a new query when re-mounting with cacheTime 0', async () => {
it('should pick up a query when re-mounting with cacheTime 0', async () => {
const key = queryKey()
const states: UseQueryResult<string>[] = []

function Page() {
const [toggle, setToggle] = React.useState(false)

React.useEffect(() => {
setActTimeout(() => {
setToggle(true)
}, 20)
}, [setToggle])

return toggle ? <Component key="1" /> : <Component key="2" />
return (
<div>
<button onClick={() => setToggle(true)}>toggle</button>
{toggle ? (
<Component key="2" value="2" />
) : (
<Component key="1" value="1" />
)}
</div>
)
}

function Component() {
function Component({ value }: { value: string }) {
const state = useQuery(
key,
async () => {
await sleep(5)
return 'data'
await sleep(10)
return 'data: ' + value
},
{
cacheTime: 0,
notifyOnChangeProps: 'all',
}
)
states.push(state)
return null
return (
<div>
<div>{state.data}</div>
</div>
)
}

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

await sleep(100)
await rendered.findByText('data: 1')

expect(states.length).toBe(5)
rendered.getByRole('button', { name: /toggle/i }).click()

await rendered.findByText('data: 2')

expect(states.length).toBe(4)
// First load
expect(states[0]).toMatchObject({ isLoading: true, isSuccess: false })
expect(states[0]).toMatchObject({
isLoading: true,
isSuccess: false,
isFetching: true,
})
// First success
expect(states[1]).toMatchObject({ isLoading: false, isSuccess: true })
// Switch
expect(states[2]).toMatchObject({ isLoading: false, isSuccess: true })
// Second load
expect(states[3]).toMatchObject({ isLoading: true, isSuccess: false })
expect(states[1]).toMatchObject({
isLoading: false,
isSuccess: true,
isFetching: false,
})
// Switch, goes to fetching
expect(states[2]).toMatchObject({
isLoading: false,
isSuccess: true,
isFetching: true,
})
// Second success
expect(states[4]).toMatchObject({ isLoading: false, isSuccess: true })
expect(states[3]).toMatchObject({
isLoading: false,
isSuccess: true,
isFetching: false,
})
})

it('should not get into an infinite loop when removing a query with cacheTime 0 and rerendering', async () => {
Expand Down Expand Up @@ -5097,64 +5123,6 @@ describe('useQuery', () => {

onlineMock.mockRestore()
})

it('online queries with cacheTime:0 should not fetch if paused and then unmounted', async () => {
const key = queryKey()
let count = 0

function Component() {
const state = useQuery({
queryKey: key,
queryFn: async () => {
count++
await sleep(10)
return 'data' + count
},
cacheTime: 0,
})

return (
<div>
<div>
status: {state.status}, fetchStatus: {state.fetchStatus}
</div>
<div>data: {state.data}</div>
</div>
)
}

function Page() {
const [show, setShow] = React.useState(true)

return (
<div>
{show && <Component />}
<button onClick={() => setShow(false)}>hide</button>
</div>
)
}

const onlineMock = mockNavigatorOnLine(false)

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

await waitFor(() =>
rendered.getByText('status: loading, fetchStatus: paused')
)

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

onlineMock.mockReturnValue(true)
window.dispatchEvent(new Event('online'))

await sleep(15)

expect(queryClient.getQueryState(key)).not.toBeDefined()

expect(count).toBe(0)

onlineMock.mockRestore()
})
})

describe('networkMode always', () => {
Expand Down
6 changes: 0 additions & 6 deletions src/reactjs/useBaseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,6 @@ export function useBaseQuery<
if (typeof defaultedOptions.staleTime !== 'number') {
defaultedOptions.staleTime = 1000
}

// Set cache time to 1 if the option has been set to 0
// when using suspense to prevent infinite loop of fetches
if (defaultedOptions.cacheTime === 0) {
defaultedOptions.cacheTime = 1
}
Comment on lines -62 to -67
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arnaudbzn I think I finally found a way to remove this suspense workaround, albeit by adding another workaround. Can you maybe review this PR? I've left a bit of information in the commit messages

}

if (defaultedOptions.suspense || defaultedOptions.useErrorBoundary) {
Expand Down