Skip to content

Commit

Permalink
v2
Browse files Browse the repository at this point in the history
(#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 <fredrik.hoglund@gmail.com>
Co-authored-by: Pepijn Senders <PepijnSenders@users.noreply.github.com>
Co-authored-by: Jack <jack.m.ellis@hotmail.co.uk>
Co-authored-by: Jack Ellis <jack.ellis@godaddy.com>
Co-authored-by: Jake Ginnivan <jake@ginnivan.net>
  • Loading branch information
tannerlinsley committed Jun 22, 2020
1 parent 4a37d1b commit 84d1a19
Show file tree
Hide file tree
Showing 44 changed files with 3,055 additions and 3,829 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test-and-publish.yml
Expand Up @@ -29,7 +29,7 @@ jobs:
publish-module:
name: 'Publish Module to NPM'
needs: test
if: github.ref == 'refs/heads/master' #publish only when merged in master, not on PR
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/next' #publish only when merged in master, not on PR
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
Expand Down
453 changes: 0 additions & 453 deletions CHANGELOG.md

This file was deleted.

855 changes: 355 additions & 500 deletions README.md

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion examples/auto-refetching/pages/api/data.js
@@ -1,13 +1,16 @@
// an simple endpoint for getting current list
let list = ['Item 1', 'Item 2', 'Item 3']

export default (req, res) => {
export default async (req, res) => {
if (req.query.add) {
if (!list.includes(req.query.add)) {
list.push(req.query.add)
}
} else if (req.query.clear) {
list = []
}

await new Promise(r => setTimeout(r, 100))

res.json(list)
}
39 changes: 34 additions & 5 deletions examples/auto-refetching/pages/index.js
Expand Up @@ -5,30 +5,31 @@ 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('')

const { status, data, error } = useQuery(
const { status, data, error, isFetching } = useQuery(
'todos',
async () => {
const { data } = await axios.get('/api/data')
return data
},
{
// Refetch the data every second
refetchInterval: 1000,
refetchInterval: intervalMs,
}
)

const [mutateAddTodo] = useMutation(
value => fetch(`/api/data?add=${value}`),
{
onSuccess: () => queryCache.refetchQueries('todos'),
onSuccess: () => queryCache.invalidateQueries('todos'),
}
)

const [mutateClear] = useMutation(value => fetch(`/api/data?clear=1`), {
onSuccess: () => queryCache.refetchQueries('todos'),
onSuccess: () => queryCache.invalidateQueries('todos'),
})

if (status === 'loading') return <h1>Loading...</h1>
Expand All @@ -42,6 +43,27 @@ export default () => {
multiple tabs to the same localhost server and see your changes
propagate between the two.
</p>
<label>
Query Interval speed (ms):{' '}
<input
value={intervalMs}
onChange={ev => setIntervalMs(Number(ev.target.value))}
type="number"
step="100"
/>{' '}
<span
style={{
display: 'inline-block',
marginLeft: '.5rem',
width: 10,
height: 10,
background: isFetching ? 'green' : 'transparent',
transition: !isFetching ? 'all .3s ease' : 'none',
borderRadius: '100%',
transform: 'scale(2)',
}}
/>
</label>
<h2>Todo List</h2>
<form
onSubmit={async ev => {
Expand Down Expand Up @@ -69,3 +91,10 @@ export default () => {
</div>
)
}

export default () => (
<>
<App />
<App />
</>
)
54 changes: 35 additions & 19 deletions examples/basic/src/index.js
Expand Up @@ -2,21 +2,7 @@
import React from "react";
import ReactDOM from "react-dom";
import axios from "axios";
import { useQuery } from "react-query";

const getPosts = async () => {
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/posts"
);
return data;
};

const getPostById = async (key, id) => {
const { data } = await axios.get(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
return data;
};
import { useQuery, queryCache } from "react-query";

function App() {
const [postId, setPostId] = React.useState(-1);
Expand All @@ -42,8 +28,22 @@ function App() {
);
}

// This function is not inline to show how query keys are passed to the query function
// Normally, you can inline them if you want.
const getPostById = async (key, id) => {
const { data } = await axios.get(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
return data;
};

function Posts({ setPostId }) {
const { status, data, error, isFetching } = useQuery("posts", getPosts);
const { status, data, error, isFetching } = useQuery("posts", async () => {
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/posts"
);
return data;
});

return (
<div>
Expand All @@ -58,7 +58,20 @@ function Posts({ setPostId }) {
<div>
{data.map(post => (
<p key={post.id}>
<a onClick={() => setPostId(post.id)} href="#">
<a
onClick={() => setPostId(post.id)}
href="#"
style={
// We can use the queryCache here to show bold links for
// ones that are cached
queryCache.getQueryData(["post", post.id])
? {
fontWeight: "bold",
color: "green"
}
: {}
}
>
{post.title}
</a>
</p>
Expand All @@ -74,8 +87,11 @@ function Posts({ setPostId }) {

function Post({ postId, setPostId }) {
const { status, data, error, isFetching } = useQuery(
postId && ["post", postId],
getPostById
["post", postId],
getPostById,
{
enabled: postId
}
);

return (
Expand Down
16 changes: 15 additions & 1 deletion examples/custom-hooks/src/index.js
@@ -1,6 +1,7 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
import React from "react";
import ReactDOM from "react-dom";
import { queryCache } from "react-query";

import usePosts from "./hooks/usePosts";
import usePost from "./hooks/usePost";
Expand Down Expand Up @@ -41,7 +42,20 @@ function Posts({ setPostId }) {
<div>
{data.map(post => (
<p key={post.id}>
<a onClick={() => setPostId(post.id)} href="#">
<a
onClick={() => setPostId(post.id)}
href="#"
style={
// We can use the queryCache here to show bold links for
// ones that are cached
queryCache.getQueryData(["post", post.id])
? {
fontWeight: "bold",
color: "green"
}
: {}
}
>
{post.title}
</a>
</p>
Expand Down
4 changes: 2 additions & 2 deletions examples/focus-refetching/pages/index.js
Expand Up @@ -10,11 +10,11 @@ export default () => {
})

const [logoutMutation] = useMutation(logout, {
onSuccess: () => queryCache.refetchQueries('user'),
onSuccess: () => queryCache.invalidateQueries('user'),
})

const [loginMutation] = useMutation(login, {
onSuccess: () => queryCache.refetchQueries('user'),
onSuccess: () => queryCache.invalidateQueries('user'),
})

return (
Expand Down
4 changes: 2 additions & 2 deletions examples/load-more-infinite-scroll/pages/api/projects.js
Expand Up @@ -2,7 +2,7 @@
export default (req, res) => {
const cursor = parseInt(req.query.cursor) || 0

const data = Array(3)
const data = Array(5)
.fill(0)
.map((_, i) => {
return {
Expand All @@ -11,7 +11,7 @@ export default (req, res) => {
}
})

const nextId = cursor < 9 ? data[data.length - 1].id + 1 : null
const nextId = cursor < 10 ? data[data.length - 1].id + 1 : null

setTimeout(() => res.json({ data, nextId }), 1000)
}
2 changes: 1 addition & 1 deletion examples/load-more-infinite-scroll/pages/index.js
Expand Up @@ -51,7 +51,7 @@ export default () => {
style={{
border: '1px solid gray',
borderRadius: '5px',
padding: '5rem 1rem',
padding: '10rem 1rem',
}}
key={project.id}
>
Expand Down
8 changes: 4 additions & 4 deletions examples/optimistic-updates/pages/index.js
Expand Up @@ -17,6 +17,7 @@ export default () => {
// the old value and return it so that it's accessible in case of
// an error
onMutate: text => {
setText('')
queryCache.cancelQueries('todos')

const previousValue = queryCache.getQueryData('todos')
Expand All @@ -31,11 +32,10 @@ export default () => {
// On failure, roll back to the previous value
onError: (err, variables, previousValue) =>
queryCache.setQueryData('todos', previousValue),
onSuccess: () => {
setText('')
},
// After success or failure, refetch the todos query
onSettled: () => queryCache.refetchQueries('todos'),
onSettled: () => {
queryCache.invalidateQueries('todos')
},
}
)

Expand Down
34 changes: 17 additions & 17 deletions examples/pagination/pages/index.js
@@ -1,29 +1,29 @@
import React from "react";
import axios from "axios";
import { usePaginatedQuery, queryCache } from "react-query";
import React from 'react'
import axios from 'axios'
import { usePaginatedQuery, queryCache } from 'react-query'

function Todos() {
const [page, setPage] = React.useState(0);
const [page, setPage] = React.useState(0)

const fetchProjects = React.useCallback(async (key, page = 0) => {
const { data } = await axios.get("/api/projects?page=" + page);
return data;
}, []);
const { data } = await axios.get('/api/projects?page=' + page)
return data
}, [])

const {
status,
resolvedData,
latestData,
error,
isFetching
} = usePaginatedQuery(["projects", page], fetchProjects, {});
isFetching,
} = usePaginatedQuery(['projects', page], fetchProjects, {})

// Prefetch the next page!
React.useEffect(() => {
if (latestData?.hasMore) {
queryCache.prefetchQuery(["projects", page + 1], fetchProjects);
queryCache.prefetchQuery(['projects', page + 1], fetchProjects)
}
}, [latestData, fetchProjects, page]);
}, [latestData, fetchProjects, page])

return (
<div>
Expand All @@ -35,9 +35,9 @@ function Todos() {
instantaneously while they are also refetched invisibly in the
background.
</p>
{status === "loading" ? (
{status === 'loading' ? (
<div>Loading...</div>
) : status === "error" ? (
) : status === 'error' ? (
<div>Error: {error.message}</div>
) : (
// `resolvedData` will either resolve to the latest page's data
Expand All @@ -54,7 +54,7 @@ function Todos() {
disabled={page === 0}
>
Previous Page
</button>{" "}
</button>{' '}
<button
onClick={() =>
// Here, we use `latestData` so the Next Page
Expand All @@ -68,9 +68,9 @@ function Todos() {
{// Since the last page's data potentially sticks around between page requests,
// we can use `isFetching` to show a background loading
// indicator since our `status === 'loading'` state won't be triggered
isFetching ? <span> Loading...</span> : null}{" "}
isFetching ? <span> Loading...</span> : null}{' '}
</div>
);
)
}

export default Todos;
export default Todos

0 comments on commit 84d1a19

Please sign in to comment.