Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refetch interval fix #589

Closed
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
81 changes: 81 additions & 0 deletions src/queryCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getQueryArgs,
deepIncludes,
noop,
Console,
} from './utils'
import { defaultConfigRef } from './config'

Expand Down Expand Up @@ -324,6 +325,52 @@ export function makeQueryCache() {
query.cancelled = null
}

// These values are used to handle auto refetching.

// The result of calling setInterval.
let currentRefetchIntervalId = null;
// The current interval in milliseconds .
let currentRefetchInterval = null;
// A map like { instanceId: true, anotherInstanceId: true }.
// If an instance ID is present among the keys,
// it means that instance expects auto refetching to work
// in the background.
let instancesRequestingBackgroundRefetch = {}
// Each instance writes here its desired interval, like
// { instanceId: 300, anotherInstanceId: 500 }
let refetchIntervalsRequestedByInstances = {}

// This function should be called every time some preferences
// regarding auto refetching change.
// It controls the timing and behavior of auto refetching.
const adjustRefreshInterval = () => {

// The shortest requested interval wins.
const requestedInterval = (
Object.values(refetchIntervalsRequestedByInstances).length > 0
? Math.min(...Object.values(refetchIntervalsRequestedByInstances))
: null
);

// If the interval has changed since the last time we checked.
if (requestedInterval !== currentRefetchInterval) {
// Stop the old auto refetching.
clearInterval(currentRefetchIntervalId);

// If the new interval is truthy, enable the new auto refetching.
if (requestedInterval) {
currentRefetchIntervalId = setInterval(() => {
// At least one instance expects auto refetching to work in the background.
const shouldRefetchInBackground = Object.keys(instancesRequestingBackgroundRefetch).length > 0;
if (isDocumentVisible() || shouldRefetchInBackground) {
query.refetch().catch(Console.error)
}
}, requestedInterval);
currentRefetchInterval = requestedInterval;
}
}
}

query.cancel = () => {
query.cancelled = cancelledError

Expand Down Expand Up @@ -355,9 +402,13 @@ export function makeQueryCache() {

// Return the unsubscribe function
return () => {
delete instancesRequestingBackgroundRefetch[instanceId];
delete refetchIntervalsRequestedByInstances[instanceId];
adjustRefreshInterval();
query.instances = query.instances.filter(d => d.id !== instanceId)

if (!query.instances.length) {
clearTimeout(currentRefetchIntervalId);
query.cancel()

// Schedule garbage collection
Expand Down Expand Up @@ -507,6 +558,36 @@ export function makeQueryCache() {
return query.promise
}

query.refetch = async ({ throwOnError, ...rest } = {}) => {
try {
return await query.fetch(rest)
} catch (err) {
if (throwOnError) {
throw err
}
}
}

query.setRefetchInBackground = (instanceId, shouldRefetchInBackground) => {
if (shouldRefetchInBackground) {
instancesRequestingBackgroundRefetch[instanceId] = true;
} else {
delete instancesRequestingBackgroundRefetch[instanceId];
}

adjustRefreshInterval();
}

query.setRefetchInterval = (instanceId, refetchInterval) => {
if (refetchInterval) {
refetchIntervalsRequestedByInstances[instanceId] = refetchInterval;
} else {
delete refetchIntervalsRequestedByInstances[instanceId];
}

adjustRefreshInterval();
}

query.setState = updater => dispatch({ type: actionSetState, updater })

query.setData = updater => {
Expand Down
76 changes: 76 additions & 0 deletions src/tests/useQuery.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
useQueryCache,
makeQueryCache,
ReactQueryCacheProvider,
ReactQueryConfigProvider,
} from '../index'
import { sleep } from './utils'

Expand Down Expand Up @@ -1066,4 +1067,79 @@ describe('useQuery', () => {
await sleep(110);
expect(container.firstChild.textContent).toEqual('count: 5')
})

it('respects the shortest active refetch interval', async () => {
let currentNumber = 0;
const fetchNumber = () => {
return currentNumber++;
}

function NumberConsumer1() {
const { data: number } = useQuery(
['number'],
fetchNumber,
{ refetchInterval: 300 }
);
return `consumer 1: ${number}`
}

function NumberConsumer2() {
const { data: number } = useQuery(
['number'],
fetchNumber,
{ refetchInterval: 1000 }
);
return `consumer 2: ${number}`
}

function Page() {
const [consumer1Visible, setConsumer1Visible] = React.useState(true);
return (<div>
{consumer1Visible && <NumberConsumer1 />}
<NumberConsumer2 />
<button onClick={() => setConsumer1Visible(false)}>hide consumer 1</button>
</div>);
}

const queryConfig = {
suspense: true
};

const { container, getByText } = render(
<ReactQueryConfigProvider config={queryConfig}>
<React.Suspense fallback={"loading"}>
<Page />
</React.Suspense>
</ReactQueryConfigProvider>
);

await waitFor(() => {
expect(container.textContent).toMatch(/consumer 1: 0/)
expect(container.textContent).toMatch(/consumer 2: 0/)
});

await sleep(301);

expect(container.textContent).toMatch(/consumer 1: 1/)
expect(container.textContent).toMatch(/consumer 2: 1/)

await sleep(301);

expect(container.textContent).toMatch(/consumer 1: 2/)
expect(container.textContent).toMatch(/consumer 2: 2/)

fireEvent.click(getByText(/hide consumer 1/))

await waitFor(() => {
expect(container.textContent).not.toMatch(/consumer 1:/)
});

await sleep(301);

expect(container.textContent).toMatch(/consumer 2: 2/)

await sleep(1500);

expect(container.textContent).toMatch(/consumer 2: 3/)
})
})
50 changes: 11 additions & 39 deletions src/useBaseQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { useQueryCache } from './queryCache'
import { useConfigContext } from './config'
import {
useUid,
isDocumentVisible,
Console,
useGetLatest,
useMountedCallback,
Expand Down Expand Up @@ -49,18 +48,6 @@ export function useBaseQuery(queryKey, queryVariables, queryFn, config = {}) {
const rerender = useMountedCallback(unsafeRerender)

const getLatestConfig = useGetLatest(config)
const refetch = React.useCallback(
async ({ throwOnError, ...rest } = {}) => {
try {
return await query.fetch(rest)
} catch (err) {
if (throwOnError) {
throw err
}
}
},
[query]
)

query.suspenseInstance = {
onSuccess: data => getLatestConfig().onSuccess(data),
Expand Down Expand Up @@ -91,42 +78,27 @@ export function useBaseQuery(queryKey, queryVariables, queryFn, config = {}) {
query.state.isStale && // Only refetch if stale
(getLatestConfig().refetchOnMount || query.instances.length === 1)
) {
refetch().catch(Console.error)
query.refetch().catch(Console.error)
}

query.wasPrefetched = false
query.wasSuspended = false
}, [getLatestConfig, query, refetch])
}, [getLatestConfig, query])

// Handle refetch interval
// Save the refetch interval requested by this instance in the shared query object.
React.useEffect(() => {
const query = queryRef.current
if (
config.refetchInterval &&
(!query.currentRefetchInterval ||
// shorter interval should override previous one
config.refetchInterval < query.currentRefetchInterval)
) {
query.currentRefetchInterval = config.refetchInterval
clearInterval(query.refetchIntervalId)
query.refetchIntervalId = setInterval(() => {
if (isDocumentVisible() || config.refetchIntervalInBackground) {
refetch().catch(Console.error)
}
}, config.refetchInterval)

return () => {
clearInterval(query.refetchIntervalId)
delete query.refetchIntervalId
delete query.currentRefetchInterval
}
}
}, [config.refetchInterval, config.refetchIntervalInBackground, refetch])
query.setRefetchInterval(instanceId, config.refetchInterval);
}, [instanceId, query, config.refetchInterval]);

// Save the background fetching preferences of this instance in the shared query object.
React.useEffect(() => {
query.setRefetchInBackground(instanceId, config.refetchIntervalInBackground);
}, [instanceId, query, config.refetchIntervalInBackground]);

return {
...query.state,
config,
query,
refetch,
refetch: query.refetch,
}
}