Skip to content

Commit

Permalink
fix: different refetchIntervals per instance use the min
Browse files Browse the repository at this point in the history
  • Loading branch information
tannerlinsley committed Jun 18, 2020
1 parent b6314ab commit b9168b8
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 19 deletions.
9 changes: 8 additions & 1 deletion examples/auto-refetching/pages/index.js
Expand Up @@ -5,7 +5,7 @@ import axios from 'axios'

import { useQuery, useMutation, queryCache } from 'react-query'

export default () => {
function App() {
const [intervalMs, setIntervalMs] = React.useState(1000)
const [value, setValue] = React.useState('')

Expand Down Expand Up @@ -91,3 +91,10 @@ export default () => {
</div>
)
}

export default () => (
<>
<App />
<App />
</>
)
56 changes: 38 additions & 18 deletions src/queryCache.js
Expand Up @@ -210,18 +210,6 @@ export function makeQueryCache({ frozen = isServer, defaultConfig } = {}) {
},
}

if (!isServer) {
query.cancelInterval()
if (config.refetchInterval) {
query.currentRefetchInterval = config.refetchInterval
query.refetchIntervalId = setInterval(() => {
if (isDocumentVisible() || config.refetchIntervalInBackground) {
query.fetch()
}
}, config.refetchInterval)
}
}

return query
}

Expand Down Expand Up @@ -344,19 +332,17 @@ export function makeQueryCache({ frozen = isServer, defaultConfig } = {}) {
query.cancel = () => {
query.cancelled = cancelledError

query.cancelInterval()

if (query.cancelPromises) {
query.cancelPromises()
}

delete query.promise
}

query.cancelInterval = () => {
clearInterval(query.refetchIntervalId)
delete query.refetchIntervalId
delete query.currentRefetchInterval
query.clearIntervals = () => {
query.instances.forEach(instance => {
instance.clearInterval()
})
}

query.setState = updater =>
Expand All @@ -373,6 +359,7 @@ export function makeQueryCache({ frozen = isServer, defaultConfig } = {}) {
query.clear = () => {
clearTimeout(query.staleTimeout)
clearTimeout(query.cacheTimeout)
query.clearIntervals()
query.cancel()
query.dispatch = noop
delete queryCache.queries[query.queryHash]
Expand All @@ -388,8 +375,40 @@ export function makeQueryCache({ frozen = isServer, defaultConfig } = {}) {

query.heal()

instance.clearInterval = () => {
clearInterval(instance.refetchIntervalId)
delete instance.refetchIntervalId
}

instance.updateConfig = config => {
const oldConfig = instance.config

// Update the config
instance.config = config

if (!isServer) {
if (oldConfig?.refetchInterval === config.refetchInterval) {
return
}

query.clearIntervals()

const minInterval = Math.min(

This comment has been minimized.

Copy link
@relayrlukaszmakuch

relayrlukaszmakuch Jun 19, 2020

This value will be calculated and used only when an instance's config updates.

Imagine we have two components:
Component A requests data every 1 second.
Component B requests data every 5 seconds.

1 second wins. It's the interval.

Then we remove Component B.

But because Component A's config hasn't changed, the interval is not updated, and it's still 1 second.

In other words, reducing the frequency of auto refetching doesn't work.

Here's a test case that help to find this and similar issues:

  it.only('respects the shortest active refetch interval', async () => {

    let currentNumber = 0;
    const fetchNumber = () => {
      return currentNumber++;
    }

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

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

    const NumberConsumer3 = React.memo(() => {
      const { data: number } = useQuery(
        ['number'],
        fetchNumber
      );
      return `consumer 3: ${number}`
    });

    function Page() {
      const [consumer1Visible, setConsumer1Visible] = React.useState(true);
      return (<div>
        {consumer1Visible && <NumberConsumer1 />}
        <NumberConsumer2 />
        <NumberConsumer3 />
        <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/)
        expect(container.textContent).toMatch(/consumer 3: 0/)
      });

      await sleep(301);

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

      await sleep(301);

      expect(container.textContent).toMatch(/consumer 1: 2/)
      expect(container.textContent).toMatch(/consumer 2: 2/)
      expect(container.textContent).toMatch(/consumer 3: 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/)
      expect(container.textContent).toMatch(/consumer 3: 2/)

      await sleep(1500);

      expect(container.textContent).toMatch(/consumer 2: 3/)
      expect(container.textContent).toMatch(/consumer 3: 3/)
  })
...query.instances.map(d => d.config.refetchInterval || Infinity)
)

if (
!instance.refetchIntervalId &&
minInterval > 0 &&
minInterval < Infinity
) {
instance.refetchIntervalId = setInterval(() => {
if (isDocumentVisible() || config.refetchIntervalInBackground) {

This comment has been minimized.

Copy link
@relayrlukaszmakuch

relayrlukaszmakuch Jun 19, 2020

Shouldn't we handle refetchIntervalInBackground the same way we handle refetchInterval, that is by deriving a config that satisfies the needs of all instances?

If I'm not mistaken, now the latest config wins.
So if component A wants to receive updates in the background and component B doesn't care, then component A won't receive its updates.

This comment has been minimized.

Copy link
@tannerlinsley

tannerlinsley Jun 19, 2020

Author Collaborator

That's a good point. I'll look into that as well.

query.fetch()
}
}, minInterval)
}
}
}

instance.run = async () => {
Expand All @@ -416,6 +435,7 @@ export function makeQueryCache({ frozen = isServer, defaultConfig } = {}) {
query.instances = query.instances.filter(d => d.id !== instance.id)

if (!query.instances.length) {
query.clearIntervals()
query.cancel()

if (!isServer) {
Expand Down

0 comments on commit b9168b8

Please sign in to comment.