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

v2 #585

Merged
merged 30 commits into from
Jun 22, 2020
Merged

v2 #585

merged 30 commits into from
Jun 22, 2020

Conversation

tannerlinsley
Copy link
Collaborator

@tannerlinsley tannerlinsley commented Jun 15, 2020

Summary

  • Better query invalidation & refetching, less over-fetching
  • Simplified dependent query syntax and new idle query state
  • Multi-instance polling and interval support
  • New query status booleans
  • Bidirectional Infinite Queries
  • Improved mutation lifecycle
  • Better test cleanup utils
  • Prep for future SSR enhancements

Breaking Changes & Migration Guide

  • Do you use falsy query keys for dependent queries?

    • The new way to do dependent queries to instead use the enabled config flag. You should move your conditions in your key to the enabled option
      • Before
      useQuery(ready && queryKey, queryFn)
      • After
      useQuery(queryKey, queryFn, { enabled: ready })
  • Do you use functions as queryKeys for dependent queries?

    • If you use functions as query keys that can potentially throw, you should migrate their logic to the enabled config option and use optional chaining to cast them to a boolean
      • Before
      useQuery(() => ['user', user.id], queryFn)
      • After
      useQuery(['user', user?.id], queryFn, { enabled: user?.id })
  • Do you expect dependent queries to start in the success state?

    • Dependent queries now start in a new idle state. You should account for this new state where possible. Most rendering logic should still work if you are checking first for the loading and error states first, then falling back to the "success" state, but still... it's a good idea to do this

      • Before
      const { status, data } = useQuery(key, fn)
      
      return status === 'loading'
        ? 'Loading...'
        : status === 'error'
        ? error.message
        : data
        ? 'The Data'
        : 'Not Ready'
      • After
      const { status } = useQuery(key, fn
      
      return status === 'idle' ? 'Not Ready' : status === 'loading'
        ? 'Loading...'
        : status === 'error'
        ? error.message
        : 'The Data'
  • Do you use queryCache.refetchQueries?

    • refetchQueries has been renamed to invalidateQueries. You will need make this rename change for your app to continue working propertly. The name change comes due to some differences in what the function does.
      • Before, any queries that matched the queryKey used in refetchQueries(queryKey) and were also stale, would be refetched... **even queries that were inactive and not rendered on the screen. This resulted in quite a few queries being refetched regardless of their immediate necessity.
      • Now, with invalidateQueries, only queries that are actively rendered will be refetched, while any other matching queries will forcefully be marked as stale.
      • This probably won't affect much performance, but should help reduce overfetching out of the box.
  • Did you expect queryCache.refetchQueries to only refetch stale queries?

    • The new invalidateQueries method will always refetch matching queries that are active. All other matched queries that are not active will be immediately marked as stale.
  • Do you call queryCache.refetchQueries with the force option?

    • Before, the force option was a way to force queries that were not stale to refetch.
    • Now, he new invalidateQueries method will always refetch matching queries that are active and all other matched queries that are not active will be immediately marked as stale.
  • Do you use a global configuration object to configure React Query?

    • Before, the global configuration object was flat:
      const globalConfig = {
        suspense,
        useErrorBoundary,
        throwOnError,
        refetchAllOnWindowFocus,
        queryKeySerializerFn,
        onMutate,
        onSuccess,
        onError,
        onSettled,
        retry,
        retryDelay,
        staleTime,
        cacheTime,
        refetchInterval,
        queryFnParamsFilter,
        refetchOnMount,
        isDataEqual,
      }
    • Now, the global configuration object has 3 parts. The shared section, which is a base set of options that are inherited into the next 2 sections, queries and mutations, each corresponding to the functionality they are used for in React Query:
      const globalConfig = {
        shared: {
          suspense,
          queryKeySerializerFn,
        },
        queries: {
          ...shared,
          enabled,
          retry,
          retryDelay,
          staleTime,
          cacheTime,
          refetchOnWindowFocus,
          refetchInterval,
          queryFnParamsFilter,
          refetchOnMount,
          isDataEqual,
          onError,
          onSuccess,
          onSettled,
          throwOnError,
          useErrorBoundary,
        },
        mutations: {
          ...shared,
          throwOnError,
          onMutate,
          onError,
          onSuccess,
          onSettled,
          useErrorBoundary,
        },
      }
  • Do you use "optional query variables" eg. useQuery(queryKey, optionalVariables, queryFn) or useQuery({ variables })?

    • Optional variables have been removed. They were not used by many and also were unnecessary seeing how you can inline them into your query function
    • Before
      useQuery('todos', [optional, variables], queryFn)
    • After
      useQuery('todos', (key) => queryFn(key, optional, variables))
  • Do you use the globalConfig.refetchAllOnWindowFocus config option?

    • refetchAllOnWindowFocus has been renamed to refetchOnWindowFocus to match the option in the configuration object for useQuery and friends.
  • Do you use the refetch function returned by useQuery and friends?

    • Previously this refetch function would not trigger an actual refetch if the query is not stale.
    • Now, calling this refetch will always trigger a refetch, regardless if the query is stale or not.
  • Do you expect prefetchQuery to skip the first render of useQuery instances that render after it?

    • Previously, the first useQuery call after a prefetchQuery would be skipped all the time.
    • Now, the staleTime of a prefetchQuery instance is honored. So, if you call prefetchQuery(key, fn, { staletime: 5000 }), and then useQuery(key, fn) is rendered within those initial 5000 milliseconds, the query will not refetch in the background, since it is not stale yet. Likewise, if the stale time has been reached by the time useQuery(key, fn) renders, it will refetch in the background, since it is stale when useQuery mounts/renders.
  • Do you use prefetchQueries' throwOnError or force options?

    • prefetchQuery's throwOnError and force options are now located in a fourth argument, after the query config.
    • Before
      prefetchQuery(key, fn, { ...queryConfig, throwOnError: true, force: true })
    • After
      prefetchQuery(key, fn, queryConfig, { throwOnError: true, force: true })
  • Do you call mutate() with additional side-effect callbacks? eg. mutate(vars, { onError, onSuccess, onSettled })

    • There are no code changes here, however previously, mutate()-level side effects would run before side-effects defined in useMutation. That order has been reversed to make more sense.

    • Now, the side-effect callbacks in useMutation will be fired before their mutate-level counterparts.

    • Before

      const mutate = useMutation(fn, {
        onSuccess: () => console.log('I will run second'),
      })
      
      mutate(vars, { onSuccess: () => console.log('I will run first') })
    • After

      const mutate = useMutation(fn, {
        onSuccess: () => console.log('I will run first'),
      })
      
      mutate(vars, { onSuccess: () => console.log('I will run second') })
  • Do you use setQueryData to update multiple queries with a single query key? eg. setQueryData('todos', newData) and expect queryKeys ['todos', 1] and ['todos', 2] to both get updated?

    • setQueryData no longer allows updating multiple queries at once (via prefix matching). If you need to update multiple queries with the same data, you can use the queryCache.getQueries() function to match all of the queries you want, then loop over them and use their query.setData function to set all of them to the same value.

New Features

  • The booleans isSuccess, isError isLoading and a new one called isIdle have been added to the queryInfo object returned by useQuery (and friends) and useMutation. These are derived safely from the queryInfo.status and are guaranteed to not overlap. In most situations, they are easier to use, less typo-prone than status strings and also more terse for determining what to render based on the status of a query.

    const queryInfo = useQuery(queryKey, fn)
    
    return queryInfo.isLoading ? (
      'Loading...'
    ) : queryInfo.isError ? (
      queryInfo.error.message
    ) : (
      <div>{queryInfo.data}</div>
    )
  • queryCaches is now exported, which allows you to clean up all query caches that were created. We do this in our own tests when multiple caches are used for testing and to be thorough.

    // test.js
    import { queryCaches } from 'react-query'
    
    afterEach(() => {
      queryCaches.forEach((queryCache) => queryCache.clear())
    })
  • fetchMore now supports an optional previous option, which will determine if the data you are fetching is should be prepended instead of appended to your infinite list. eg, fetchMore(nextPageVars, { previous: true })

    const { fetchMore } = useInfiniteQuery(queryKey, fn)
    
    return (
      <button
        onClick={() => fetchMore(previousPageVariables, { previous: true })}
      />
    )
  • refetchInterval can now be changed on the fly. Check out the auto-refetching example to see it in action!

  • invalidateQueries (previously refetchQueries) now has an option called refetchActive that when set to false will not refetch matching queries that are active on the page.

  • makeQueryCache now accepts an optional configuration object. The defaultConfig object is used to override the default query configuration config to use inside of this cache. The frozen option if set to true will simulate React Query being run on the server, where no queries are cached for privacy and safety concerns. This is the default when using React Query on the server and is optional. You can also set it to true on the server and have it work as it would on the client. More information on this coming soon!

tannerlinsley and others added 14 commits June 13, 2020 23:54
…reated

This change is done so we can improve support for server side rendering in the future.
* Caches created with makeQueryCache on the server now cache data
* Add options object to makeQueryCache and move defaultConfig-option into that
* Add more server guards for scheduleStale and scheduleGarbage
* Add more SSR-tests

BREAKING CHANGE: Caches created with makeQueryCache will now cache data on the server.
@tannerlinsley tannerlinsley marked this pull request as ready for review June 17, 2020 02:02
@loiacon
Copy link
Contributor

loiacon commented Jun 17, 2020

prefetchQuery's force option has been removed. When you call prefetchQuery, the queryFn will always be called, regardless of the query is stale or not.

What about creating a fetchQuery function that only will be called when query is stale? With this, we can prevent to use queryCache.get(queryKey).state.isStale in every query that we want to cache outside React:

const getTodos = () => {
  // Cached? Return todos, otherwise do request to api
  return queryCache.fetchQuery("todos", fetchTodos)
}

@tannerlinsley
Copy link
Collaborator Author

tannerlinsley commented Jun 17, 2020 via email

@loiacon
Copy link
Contributor

loiacon commented Jun 17, 2020

I think one use case that's we can include is when I want to fetch some info to use in a next page:

Think in a case we need to get some post data outside the post page, but, if the user click in the button to fetch again, don't fetch the same post twice.

const getPostData = (postId) => {
  return queryCache.fetchQuery(["post", postId], () => fetchPost(postId))
}

const SomeRandomPage = () => {
  return (
    <button onClick={() => getPostData(1)}>
      Get our first post!
    </button>
  )
}

When access this post page, we will only show his data, instead of loading the query again:

const SomePostPage = ({ postId }) => {
  const { data, status, error } = useQuery(["post", postId], () => fetchPost(postId))

  return ...
} 

Does it seem reasonable to you?

@loiacon
Copy link
Contributor

loiacon commented Jun 17, 2020

I think a more "real world" example is when we have multiple tabs on some page and want to fetch all these tabs in a unique place, avoiding fetch the other tabs data only when going to his content. If the user move to this page later, only fetch again when query is stale.

NOTE: Tab renders conditionally

const getTabsContent = (someId) => {
  queryCache.fetchQuery(["products", someId], () => fetchProducts(someId));
  queryCache.fetchQuery(["todos", someId], () => fetchTodos(someId));
};

const SomePage = () => {
  const { someId } = usePageParams();
  useEffect(() => {
    getTabsContent(someId);
  }, [someId]);

  return (
    <Tabs initialTab="Foo tab">
      <Tab name="Foo tab">
        <TabContent01 />
      </Tab>
      <Tab name="Bar tab">
        <TabContent02 />
      </Tab>
    </Tabs>
  );
};

const TabContent01 = () => {
  const { someId } = usePageParams();
  const { data, status, error } = useQuery(["products", someId], () =>
    fetchProducts(someId)
  );

  return ...
};

const TabContent02 = () => {
  const { someId } = usePageParams();
  const { data, status, error } = useQuery(["todos", someId], () =>
    fetchTodos(someId)
  );

  return ...
};

@tannerlinsley
Copy link
Collaborator Author

So the main use case around this is to be Able to prefetch data without worrying if it’s stale or not? If it doesn’t exist, prefetch it, if it does and it’s not stale, do nothing, if it does and it’s stale, trigger a background fetch?

@loiacon
Copy link
Contributor

loiacon commented Jun 17, 2020

So the main use case around this is to be Able to prefetch data without worrying if it’s stale or not? If it doesn’t exist, prefetch it, if it does and it’s not stale, do nothing, if it does and it’s stale, trigger a background fetch?

Yes, in general, I think that's it

@mwmcode
Copy link

mwmcode commented Jun 17, 2020

Now that we have isSuccess isLoading ...etc, is there a plan to drop status later on?

@tannerlinsley
Copy link
Collaborator Author

Nope. You can use either.

@tannerlinsley
Copy link
Collaborator Author

I've restored the functionality of prefetchQuery (aside from the function signature change) to allow for force again. This should cover use cases for lazily prefetching queries only if they are stale.

@tannerlinsley
Copy link
Collaborator Author

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.

The latest commits fix that. Instances are tracked individually

* fix: update type definitions

* fixup! fix: update type definitions

Co-authored-by: Jack Ellis <[email protected]>
@fschwalm
Copy link
Contributor

fschwalm commented Jun 19, 2020

There are no code changes here, however previously, mutate()-level side effects would run before side-effects defined in useMutation. That order has been reversed to make more sense.

I totally agree, i was about to open a new discussion but i found this PR.

Thx!

@tannerlinsley tannerlinsley changed the title Next v2 Jun 19, 2020
@mattcorner
Copy link

Great migration guide. Only thing I've come across while reading and migrating:

  • Documentation content link for API/invalidateQueries is broken. Still referencing refetchQueries.

Other than that it was smooth sailing 👍

@tannerlinsley tannerlinsley merged commit 03761c0 into master Jun 22, 2020
@tannerlinsley tannerlinsley deleted the next branch June 22, 2020 21:37
@tannerlinsley tannerlinsley restored the next branch June 22, 2020 21:38
@tannerlinsley tannerlinsley deleted the next branch June 22, 2020 21:38
tannerlinsley added a commit that referenced this pull request Jun 22, 2020
- Better query invalidation & refetching, less over-fetching
- Simplified dependent query syntax and new `idle` query state
- Multi-instance polling and interval support
- New query status booleans
- Bidirectional Infinite Queries
- Improved mutation lifecycle
- Better test cleanup utils
- Prep for future SSR enhancements

- The booleans `isSuccess`, `isError` `isLoading` and a new one called `isIdle` have been added to the queryInfo object returned by `useQuery` (and friends) and `useMutation`. These are derived safely from the `queryInfo.status` and are guaranteed to not overlap. In most situations, they are easier to use, less typo-prone than status strings and also more terse for determining what to render based on the status of a query.

  ```js
  const queryInfo = useQuery(queryKey, fn)

  return queryInfo.isLoading ? (
    'Loading...'
  ) : queryInfo.isError ? (
    queryInfo.error.message
  ) : (
    <div>{queryInfo.data}</div>
  )
  ```

- `queryCaches` is now exported, which allows you to clean up all query caches that were created. We do this in our own tests when multiple caches are used for testing and to be thorough.

  ```js
  // test.js
  import { queryCaches } from 'react-query'

  afterEach(() => {
    queryCaches.forEach((queryCache) => queryCache.clear())
  })
  ```

- `fetchMore` now supports an optional `previous` option, which will determine if the data you are fetching is should be prepended instead of appended to your infinite list. eg, `fetchMore(nextPageVars, { previous: true })`

  ```js
  const { fetchMore } = useInfiniteQuery(queryKey, fn)

  return (
    <button
      onClick={() => fetchMore(previousPageVariables, { previous: true })}
    />
  )
  ```

- `refetchInterval` can now be changed on the fly. Check out the auto-refetching example to see it in action!
- `invalidateQueries` (previously `refetchQueries`) now has an option called `refetchActive` that when set to `false` will **not** refetch matching queries that are active on the page.
- `makeQueryCache` now accepts an optional configuration object. The `defaultConfig` object is used to override the default query configuration config to use inside of this cache. The `frozen` option if set to `true` will simulate React Query being run on the server, where no queries are cached for privacy and safety concerns. This is the default when using React Query on the server and is optional. You can also set it to `true` on the server and have it work as it would on the client. More information on this coming soon!

BREAKING CHANGES

- Do you use falsy query keys for dependent queries?
  - The new way to do dependent queries to instead use the `enabled` config flag. You should move your conditions in your key to the `enabled` option
    - Before
    ```js
    useQuery(ready && queryKey, queryFn)
    ```
    - After
    ```js
    useQuery(queryKey, queryFn, { enabled: ready })
    ```
- Do you use functions as queryKeys for dependent queries?
  - If you use functions as query keys that can potentially throw, you should migrate their logic to the `enabled` config option and use optional chaining to cast them to a boolean
    - Before
    ```js
    useQuery(() => ['user', user.id], queryFn)
    ```
    - After
    ```js
    useQuery(['user', user?.id], queryFn, { enabled: user?.id })
    ```
- Do you expect dependent queries to start in the `success` state?

  - Dependent queries now start in a new `idle` state. You should account for this new state where possible. Most rendering logic should still work if you are checking first for the `loading` and `error` states first, then falling back to the "success" state, but still... it's a good idea to do this

    - Before

    ```js
    const { status, data } = useQuery(key, fn)

    return status === 'loading'
      ? 'Loading...'
      : status === 'error'
      ? error.message
      : data
      ? 'The Data'
      : 'Not Ready'
    ```

    - After

    ```js
    const { status } = useQuery(key, fn

    return status === 'idle' ? 'Not Ready' : status === 'loading'
      ? 'Loading...'
      : status === 'error'
      ? error.message
      : 'The Data'
    ```

- Do you use `queryCache.refetchQueries`?
  - `refetchQueries` has been renamed to `invalidateQueries`. You will need make this rename change for your app to continue working propertly. The name change comes due to some differences in what the function does.
    - Before, any queries that matched the `queryKey` used in `refetchQueries(queryKey)` and were also stale, would be refetched... \*\*even queries that were inactive and not rendered on the screen. This resulted in quite a few queries being refetched regardless of their immediate necessity.
    - Now, with `invalidateQueries`, only queries that are actively rendered will be refetched, while any other matching queries will forcefully be marked as stale.
    - This probably won't affect much performance, but should help reduce overfetching out of the box.
- Did you expect `queryCache.refetchQueries` to only refetch stale queries?
  - The new `invalidateQueries` method will **always refetch matching queries that are active**. All other matched queries that are not active will be immediately marked as stale.
- Do you call `queryCache.refetchQueries` with the `force` option?
  - Before, the `force` option was a way to force queries that were not stale to refetch.
  - Now, he new `invalidateQueries` method will **always refetch matching queries that are active** and all other matched queries that are not active will be immediately marked as stale.
- Do you use a global configuration object to configure React Query?
  - Before, the global configuration object was flat:
    ```js
    const globalConfig = {
      suspense,
      useErrorBoundary,
      throwOnError,
      refetchAllOnWindowFocus,
      queryKeySerializerFn,
      onMutate,
      onSuccess,
      onError,
      onSettled,
      retry,
      retryDelay,
      staleTime,
      cacheTime,
      refetchInterval,
      queryFnParamsFilter,
      refetchOnMount,
      isDataEqual,
    }
    ```
  - Now, the global configuration object has 3 parts. The `shared` section, which is a base set of options that are inherited into the next 2 sections, `queries` and `mutations`, each corresponding to the functionality they are used for in React Query:
    ```js
    const globalConfig = {
      shared: {
        suspense,
        queryKeySerializerFn,
      },
      queries: {
        ...shared,
        enabled,
        retry,
        retryDelay,
        staleTime,
        cacheTime,
        refetchOnWindowFocus,
        refetchInterval,
        queryFnParamsFilter,
        refetchOnMount,
        isDataEqual,
        onError,
        onSuccess,
        onSettled,
        throwOnError,
        useErrorBoundary,
      },
      mutations: {
        ...shared,
        throwOnError,
        onMutate,
        onError,
        onSuccess,
        onSettled,
        useErrorBoundary,
      },
    }
    ```
- Do you use "optional query variables" eg. `useQuery(queryKey, optionalVariables, queryFn)` or `useQuery({ variables })`?
  - Optional variables have been removed. They were not used by many and also were unnecessary seeing how you can inline them into your query function
  - Before
    ```js
    useQuery('todos', [optional, variables], queryFn)
    ```
  - After
    ```js
    useQuery('todos', (key) => queryFn(key, optional, variables))
    ```
- Do you use the `globalConfig.refetchAllOnWindowFocus` config option?
  - `refetchAllOnWindowFocus` has been renamed to `refetchOnWindowFocus` to match the option in the configuration object for `useQuery` and friends.
- Do you use the `refetch` function returned by `useQuery` and friends?
  - Previously this `refetch` function would not trigger an actual refetch if the query is not stale.
  - Now, calling this `refetch` will always trigger a refetch, regardless if the query is stale or not.
- Do you expect `prefetchQuery` to skip the first render of `useQuery` instances that render after it?
  - Previously, the first `useQuery` call after a `prefetchQuery` would be skipped all the time.
  - Now, the `staleTime` of a `prefetchQuery` instance is honored. So, if you call `prefetchQuery(key, fn, { staletime: 5000 })`, and then `useQuery(key, fn)` is rendered within those initial 5000 milliseconds, the query will **not refetch in the background, since it is not stale yet**. Likewise, if the stale time has been reached by the time `useQuery(key, fn)` renders, **it will refetch in the background**, since it is stale when `useQuery` mounts/renders.
- Do you use `prefetchQueries`' `throwOnError` or `force` options?
  - `prefetchQuery`'s `throwOnError` and `force` options are now located in a fourth argument, after the query config.
  - Before
    ```js
    prefetchQuery(key, fn, { ...queryConfig, throwOnError: true, force: true })
    ```
  - After
    ```js
    prefetchQuery(key, fn, queryConfig, { throwOnError: true, force: true })
    ```
- Do you call `mutate()` with additional side-effect callbacks? eg. `mutate(vars, { onError, onSuccess, onSettled })`

  - There are no code changes here, however previously, `mutate()`-level side effects would run _before_ side-effects defined in `useMutation`. That order has been reversed to make more sense.
  - Now, the side-effect callbacks in `useMutation` will be fired before their `mutate`-level counterparts.
  - Before

    ```js
    const mutate = useMutation(fn, {
      onSuccess: () => console.log('I will run second'),
    })

    mutate(vars, { onSuccess: () => console.log('I will run first') })
    ```

  - After

    ```js
    const mutate = useMutation(fn, {
      onSuccess: () => console.log('I will run first'),
    })

    mutate(vars, { onSuccess: () => console.log('I will run second') })
    ```

- Do you use `setQueryData` to update multiple queries with a single query key? eg. `setQueryData('todos', newData)` and expect queryKeys `['todos', 1]` and `['todos', 2]` to both get updated?
  - `setQueryData` no longer allows updating multiple queries at once (via prefix matching). If you need to update multiple queries with the same data, you can use the `queryCache.getQueries()` function to match all of the queries you want, then loop over them and use their `query.setData` function to set all of them to the same value.
Co-authored-by: Fredrik Höglund <[email protected]>
Co-authored-by: Pepijn Senders <[email protected]>
Co-authored-by: Jack <[email protected]>
Co-authored-by: Jack Ellis <[email protected]>
Co-authored-by: Jake Ginnivan <[email protected]>
tannerlinsley added a commit that referenced this pull request Jun 22, 2020
(#585)

- Better query invalidation & refetching, less over-fetching
- Simplified dependent query syntax and new `idle` query state
- Multi-instance polling and interval support
- New query status booleans
- Bidirectional Infinite Queries
- Improved mutation lifecycle
- Better test cleanup utils
- Prep for future SSR enhancements

- The booleans `isSuccess`, `isError` `isLoading` and a new one called `isIdle` have been added to the queryInfo object returned by `useQuery` (and friends) and `useMutation`. These are derived safely from the `queryInfo.status` and are guaranteed to not overlap. In most situations, they are easier to use, less typo-prone than status strings and also more terse for determining what to render based on the status of a query.

  ```js
  const queryInfo = useQuery(queryKey, fn)

  return queryInfo.isLoading ? (
    'Loading...'
  ) : queryInfo.isError ? (
    queryInfo.error.message
  ) : (
    <div>{queryInfo.data}</div>
  )
  ```

- `queryCaches` is now exported, which allows you to clean up all query caches that were created. We do this in our own tests when multiple caches are used for testing and to be thorough.

  ```js
  // test.js
  import { queryCaches } from 'react-query'

  afterEach(() => {
    queryCaches.forEach((queryCache) => queryCache.clear())
  })
  ```

- `fetchMore` now supports an optional `previous` option, which will determine if the data you are fetching is should be prepended instead of appended to your infinite list. eg, `fetchMore(nextPageVars, { previous: true })`

  ```js
  const { fetchMore } = useInfiniteQuery(queryKey, fn)

  return (
    <button
      onClick={() => fetchMore(previousPageVariables, { previous: true })}
    />
  )
  ```

- `refetchInterval` can now be changed on the fly. Check out the auto-refetching example to see it in action!
- `invalidateQueries` (previously `refetchQueries`) now has an option called `refetchActive` that when set to `false` will **not** refetch matching queries that are active on the page.
- `makeQueryCache` now accepts an optional configuration object. The `defaultConfig` object is used to override the default query configuration config to use inside of this cache. The `frozen` option if set to `true` will simulate React Query being run on the server, where no queries are cached for privacy and safety concerns. This is the default when using React Query on the server and is optional. You can also set it to `true` on the server and have it work as it would on the client. More information on this coming soon!

BREAKING CHANGE:

- Do you use falsy query keys for dependent queries?
  - The new way to do dependent queries to instead use the `enabled` config flag. You should move your conditions in your key to the `enabled` option
    - Before
    ```js
    useQuery(ready && queryKey, queryFn)
    ```
    - After
    ```js
    useQuery(queryKey, queryFn, { enabled: ready })
    ```
- Do you use functions as queryKeys for dependent queries?
  - If you use functions as query keys that can potentially throw, you should migrate their logic to the `enabled` config option and use optional chaining to cast them to a boolean
    - Before
    ```js
    useQuery(() => ['user', user.id], queryFn)
    ```
    - After
    ```js
    useQuery(['user', user?.id], queryFn, { enabled: user?.id })
    ```
- Do you expect dependent queries to start in the `success` state?

  - Dependent queries now start in a new `idle` state. You should account for this new state where possible. Most rendering logic should still work if you are checking first for the `loading` and `error` states first, then falling back to the "success" state, but still... it's a good idea to do this

    - Before

    ```js
    const { status, data } = useQuery(key, fn)

    return status === 'loading'
      ? 'Loading...'
      : status === 'error'
      ? error.message
      : data
      ? 'The Data'
      : 'Not Ready'
    ```

    - After

    ```js
    const { status } = useQuery(key, fn

    return status === 'idle' ? 'Not Ready' : status === 'loading'
      ? 'Loading...'
      : status === 'error'
      ? error.message
      : 'The Data'
    ```

- Do you use `queryCache.refetchQueries`?
  - `refetchQueries` has been renamed to `invalidateQueries`. You will need make this rename change for your app to continue working propertly. The name change comes due to some differences in what the function does.
    - Before, any queries that matched the `queryKey` used in `refetchQueries(queryKey)` and were also stale, would be refetched... \*\*even queries that were inactive and not rendered on the screen. This resulted in quite a few queries being refetched regardless of their immediate necessity.
    - Now, with `invalidateQueries`, only queries that are actively rendered will be refetched, while any other matching queries will forcefully be marked as stale.
    - This probably won't affect much performance, but should help reduce overfetching out of the box.
- Did you expect `queryCache.refetchQueries` to only refetch stale queries?
  - The new `invalidateQueries` method will **always refetch matching queries that are active**. All other matched queries that are not active will be immediately marked as stale.
- Do you call `queryCache.refetchQueries` with the `force` option?
  - Before, the `force` option was a way to force queries that were not stale to refetch.
  - Now, he new `invalidateQueries` method will **always refetch matching queries that are active** and all other matched queries that are not active will be immediately marked as stale.
- Do you use a global configuration object to configure React Query?
  - Before, the global configuration object was flat:
    ```js
    const globalConfig = {
      suspense,
      useErrorBoundary,
      throwOnError,
      refetchAllOnWindowFocus,
      queryKeySerializerFn,
      onMutate,
      onSuccess,
      onError,
      onSettled,
      retry,
      retryDelay,
      staleTime,
      cacheTime,
      refetchInterval,
      queryFnParamsFilter,
      refetchOnMount,
      isDataEqual,
    }
    ```
  - Now, the global configuration object has 3 parts. The `shared` section, which is a base set of options that are inherited into the next 2 sections, `queries` and `mutations`, each corresponding to the functionality they are used for in React Query:
    ```js
    const globalConfig = {
      shared: {
        suspense,
        queryKeySerializerFn,
      },
      queries: {
        ...shared,
        enabled,
        retry,
        retryDelay,
        staleTime,
        cacheTime,
        refetchOnWindowFocus,
        refetchInterval,
        queryFnParamsFilter,
        refetchOnMount,
        isDataEqual,
        onError,
        onSuccess,
        onSettled,
        throwOnError,
        useErrorBoundary,
      },
      mutations: {
        ...shared,
        throwOnError,
        onMutate,
        onError,
        onSuccess,
        onSettled,
        useErrorBoundary,
      },
    }
    ```
- Do you use "optional query variables" eg. `useQuery(queryKey, optionalVariables, queryFn)` or `useQuery({ variables })`?
  - Optional variables have been removed. They were not used by many and also were unnecessary seeing how you can inline them into your query function
  - Before
    ```js
    useQuery('todos', [optional, variables], queryFn)
    ```
  - After
    ```js
    useQuery('todos', (key) => queryFn(key, optional, variables))
    ```
- Do you use the `globalConfig.refetchAllOnWindowFocus` config option?
  - `refetchAllOnWindowFocus` has been renamed to `refetchOnWindowFocus` to match the option in the configuration object for `useQuery` and friends.
- Do you use the `refetch` function returned by `useQuery` and friends?
  - Previously this `refetch` function would not trigger an actual refetch if the query is not stale.
  - Now, calling this `refetch` will always trigger a refetch, regardless if the query is stale or not.
- Do you expect `prefetchQuery` to skip the first render of `useQuery` instances that render after it?
  - Previously, the first `useQuery` call after a `prefetchQuery` would be skipped all the time.
  - Now, the `staleTime` of a `prefetchQuery` instance is honored. So, if you call `prefetchQuery(key, fn, { staletime: 5000 })`, and then `useQuery(key, fn)` is rendered within those initial 5000 milliseconds, the query will **not refetch in the background, since it is not stale yet**. Likewise, if the stale time has been reached by the time `useQuery(key, fn)` renders, **it will refetch in the background**, since it is stale when `useQuery` mounts/renders.
- Do you use `prefetchQueries`' `throwOnError` or `force` options?
  - `prefetchQuery`'s `throwOnError` and `force` options are now located in a fourth argument, after the query config.
  - Before
    ```js
    prefetchQuery(key, fn, { ...queryConfig, throwOnError: true, force: true })
    ```
  - After
    ```js
    prefetchQuery(key, fn, queryConfig, { throwOnError: true, force: true })
    ```
- Do you call `mutate()` with additional side-effect callbacks? eg. `mutate(vars, { onError, onSuccess, onSettled })`

  - There are no code changes here, however previously, `mutate()`-level side effects would run _before_ side-effects defined in `useMutation`. That order has been reversed to make more sense.
  - Now, the side-effect callbacks in `useMutation` will be fired before their `mutate`-level counterparts.
  - Before

    ```js
    const mutate = useMutation(fn, {
      onSuccess: () => console.log('I will run second'),
    })

    mutate(vars, { onSuccess: () => console.log('I will run first') })
    ```

  - After

    ```js
    const mutate = useMutation(fn, {
      onSuccess: () => console.log('I will run first'),
    })

    mutate(vars, { onSuccess: () => console.log('I will run second') })
    ```

- Do you use `setQueryData` to update multiple queries with a single query key? eg. `setQueryData('todos', newData)` and expect queryKeys `['todos', 1]` and `['todos', 2]` to both get updated?
  - `setQueryData` no longer allows updating multiple queries at once (via prefix matching). If you need to update multiple queries with the same data, you can use the `queryCache.getQueries()` function to match all of the queries you want, then loop over them and use their `query.setData` function to set all of them to the same value.
Co-authored-by: Fredrik Höglund <[email protected]>
Co-authored-by: Pepijn Senders <[email protected]>
Co-authored-by: Jack <[email protected]>
Co-authored-by: Jack Ellis <[email protected]>
Co-authored-by: Jake Ginnivan <[email protected]>
@relayrlukaszmakuch
Copy link

I'm afraid this issue still persists.

If we want auto refetching to work correctly, we need to check for changes in config in more places.

In v1, I extracted a function to perform updates when needed and triggered it in places such as:

I think we may need to do something similar in v2.

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/)
  })

@relayrlukaszmakuch
Copy link

Regarding #589 (comment) , there are two challenges to tackle.

First, the code execution may never reach 286be39#diff-2c6b0e709da79e736512dd96001d9716R409 , because 286be39#diff-2c6b0e709da79e736512dd96001d9716R390 will return earlier. It's the same issues as in the comment above.

Second, updating the background refetch policy is triggered by updating the refetch interval (refetchInterval). It should be triggered by changes to the background refetch policy (refetchIntervalInBackground).

@tannerlinsley
Copy link
Collaborator Author

It should work as is. With this line:

// Update the config
instance.config = config

The config get updated regardless it anything changes or not, including the refetchIntervalInBackground option. If that options does change and there is an interval set for the query, then it will get checked here regardless on each interval tick:

query.instances.some(
  instance => instance.config.refetchIntervalInBackground
)

Beyond that, there is no reason to reset the intervals themselves unless one of them changes, which is why there is the early exit right after the config update.

Am I missing something with any of this?

@relayrlukaszmakuch
Copy link

Regarding fetching in background,

I think we should write a test for background refetching. Just to make sure that it works and to prevent any regression in the future.

Regarding the interval,

Beyond that, there is no reason to reset the intervals themselves unless one of them changes, which is why there is the early exit right after the config update.

There's a problem with the moments when this check happens.

instance.updateConfig is called when the config changes, but not, for example, when it's being removed.

That's why the test from #585 (comment) fails.

We need to update the interval every time when the following fragment evaluates to a different value:

          const minInterval = Math.min(
            ...query.instances.map(d => d.config.refetchInterval || Infinity)
          )

The task of deciding whether to update the interval should span across multiple components. It's not limited to changes in a single component's config.

BTW in a similar project I decided to build an a separate Refresher component to avoid complexity. The API was something like this:

<ItemsList/>
<Refresher key={["items"]} interval={1000 * 10} />

I found it trivial to implement and working well when you want to meet a requirement like "Oh, and on that page X, Y, and Z should be live".

You'd just drop _Refresher_s for X, Y, and Z somewhere top in the hierarchy of the page.

As far as I understand, it'd be possible to implement such a Refresher component with react-query. Then the core of the library could get much smaller.

What do you think?

@tannerlinsley
Copy link
Collaborator Author

I think it should be more than possible to tweak what's here to fit what you're talking about. What I don't understand is that the auto-refetching example shows all of this working just fine the way it is. I realize you have a test written for this, but the test isn't conveying all the info I'm looking for I guess to understand why things aren't okay the way they are. If you wanted to work up a codesandbox that shows the issue you're referring to then I'll have a better understanding of where to go.

@lukaszmakuch
Copy link

I think it should be more than possible to tweak what's here to fit what you're talking about. What I don't understand is that the auto-refetching example shows all of this working just fine the way it is. I realize you have a test written for this, but the test isn't conveying all the info I'm looking for I guess to understand why things aren't okay the way they are. If you wanted to work up a codesandbox that shows the issue you're referring to then I'll have a better understanding of where to go.

Here's a codesandbox https://codesandbox.io/s/runtime-leftpad-94dpi?file=/src/App.js that illustrates the reason why the test published above doesn't pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.