Getting started with queryies will focus on the useQuery
function.
useQuery
needs a unique key and a function that will go and grab some data.
Tanner prefers using axios
instead of fetch
as it can be cumbersome.
// with fetch import React from "react"; import { useQuery } from "react-query"; export default function App() { const queryInfo = useQuery("pokemon", () => fetch("https://pokeapi.co/api/v2/pokemon").then((res) => res.json().then((json) => json.results) ) ); return <div>Hello</div>; } // with axios import React from "react"; import { useQuery } from "react-query"; export default function App() { const queryInfo = useQuery("pokemon", () => axios("https://pokeapi.co/api/v2/pokemon").then((res) => res.data.results) ); return <div>Hello</div>; }
If we log out the queryInfo
, we can see a few logs occur as the state changes.
This includes values such as the data, loading and error state information.
queryInfo
includes status
which could be loading
| error
| success
. There is also a isLoading
, isError
and isSuccess
value.
To install, run npm i --save-dev react-query-devtools
.
// with axios import React from "react"; import { useQuery } from "react-query"; import { ReactQueryDevtools } from "react-query-devtools"; export default function App() { const queryInfo = useQuery("pokemon", () => axios("https://pokeapi.co/api/v2/pokemon").then((res) => res.data.results) ); return ( <div> Hello <ReactQueryDevtools /> </div> ); }
Note: with v3 the tools are built-in. See here for more info.
Out-of-the-box, React Query tries to keep things up to date for you. You can see refetches happen as you head back to the page.
We can update this by using the third options
solution for React Query.
useQuery( "pokemon", () => axios("https://pokeapi.co/api/v2/pokemon").then((res) => res.data.results), { refetchOnWindowFocus: false } );
What if we want an indicator to show that something is happening?
// with axios import React from "react"; import { useQuery } from "react-query"; import { ReactQueryDevtools } from "react-query-devtools"; export default function App() { const queryInfo = useQuery("pokemon", () => axios("https://pokeapi.co/api/v2/pokemon").then((res) => res.data.results) ); return ( <div> <p> {queryInfo.data.map((res) => ( <p key={result.name}>{result.name}</p> ))} </p> {queryInfo.isFetching ? "Updating..." : null} <ReactQueryDevtools /> </div> ); }
React Query has aggressive stale times. Initially, data is stale as soon as it has been fetched. We can change the refetch times to change how often things go stale.
useQuery( "pokemon", () => axios("https://pokeapi.co/api/v2/pokemon").then((res) => res.data.results), { staleTime: 5000, } );
There is a inactive
state for data when it is not shown. It also updates in the background to make sure it is up to date in the background.
If we use the cacheTime
, there will be a "garbage collection" that happens when cache data is inactive and unused.
useQuery( "pokemon", () => axios("https://pokeapi.co/api/v2/pokemon").then((res) => res.data.results), { cacheTime: 5000, } );
What happens when you have two different keys but for the same data?
We end up with different queries in our cache even though they're fetching the same data.
If both used the same user key, you'll notice that there is only one fetch that happens.
Out-of-the-box with the same key will only use that data and request once.
We can clean up how we use our hooks for multiple components.
function usePokemon() { return useQuery("pokemon", () => axios("https://pokeapi.co/api/v2/pokemon").then((res) => res.data.results) ); }
Both calls are made in parallel out-of-the-box.
This allows you to pass props or state to the query. This is useful for things such as a search box.
This is also useful to show data instantly when searches look up old queries already stored.
This can be useful to ensure no fetches are made without data.
function usePokemon({ pokemon }) { return useQuery( pokemon, () => axios("https://pokeapi.co/api/v2/pokemon").then( (res) => res.data.results ), { enabled: pokemon, } ); }
What happens when you have heaps of pokemon in a query key?
React Query knows how to stringify query keys to make more descriptive labels.
function usePokemon({ pokemon }) { return useQuery( ["pokemon", pokemon], () => axios("https://pokeapi.co/api/v2/pokemon").then( (res) => res.data.results ), { enabled: pokemon, } ); }
This is another configuration option. React Query auto-retries to see if it can get the data.
It retries with something known as "exponential backoff".
We can change retries with the retry
key.
function usePokemon({ pokemon }) { return useQuery( ["pokemon", pokemon], () => axios("https://pokeapi.co/api/v2/pokemon").then( (res) => res.data.results ), { enabled: pokemon, retry: 1, } ); }
You can also change the retry delay with the retryDelay
configuration option. You can also use a function instead of a number for retryDelay
to be more specific.
You can pass retry to be 0
or false
to stop retries.
If there is a request going out for everything you type, you can cancel the cancellation.
One option is to debounce. Out-of-the-box, React Query knows not to add the key to the store.
You can wire up query cancellation to help with this.
import axios, { CancelToken } from "axios"; function usePokemon({ pokemon }) { return useQuery( ["pokemon", pokemon], () => { const source = CancelToken.source(); const promise = axios .get("https://pokeapi.co/api/v2/pokemon", { cancelToken: source.token }) .then((res) => res.data.results); promise.cancel = () => { source.cancel("Query was cancelled by React Query"); }; return promise; }, { enabled: pokemon, retry: 1, } ); }
For fetch:
function usePokemon({ pokemon }) { return useQuery( ["pokemon", pokemon], () => { const controller = new AbortController(); const signal = controller.signal; const promise = fetch("https://pokeapi.co/api/v2/pokemon", { method: "get", signal, }).then((res) => res.json()); promise.cancel = () => { controller.abort(); }; return promise; }, { enabled: pokemon, } ); }
If we have queries that must run sequentially, we essentially need to make sure that enabled
is set for the dependent value.
Dependent query will need to make sure we have isIdle
sorted as well.
import React from "react"; import { useQuery } from "react-query"; import { ReactQueryDevtools } from "react-query-devtools"; import axios from "axios"; // user email: // https://jsonplaceholder.typicode.com/users?email=${email} // https://jsonplaceholder.typicode.com/posts?userId=${userId} const email = "Sincere@april.biz"; function MyPosts() { const userQuery = useQuery("user", () => axios .get(`https://jsonplaceholder.typicode.com/users?email=${email}`) .then((res) => res.data[0]) ); const postsQuery = useQuery( "posts", () => axios .get( `https://jsonplaceholder.typicode.com/posts?userId=${userQuery.data.id}` ) .then((res) => res.data), { enabled: userQuery.data?.id, } ); return userQuery.isLoading ? ( "Loading user..." ) : ( <div> User Id: {userQuery.data.id} <br /> <br /> {postsQuery.isIdle ? null : postsQuery.isLoading ? ( "Loading posts..." ) : ( <div>Post Count: {postsQuery.data.length}</div> )} </div> ); } export default function App() { return ( <div> <MyPosts /> <ReactQueryDevtools /> </div> ); }
This is just a config option:
const userQuery = useQuery( "user", async () => { await new Promise((resolve) => setTimeout(resolve, 1000)); return axios .get(`https://jsonplaceholder.typicode.com/users?email=${email}`) .then((res) => res.data[0]); }, { initialData: existingUser, } );
Initial data is not stale by default
const userQuery = useQuery( "user", async () => { await new Promise((resolve) => setTimeout(resolve, 1000)); return axios .get(`https://jsonplaceholder.typicode.com/users?email=${email}`) .then((res) => res.data[0]); }, { initialData: existingUser, initialStale: true, } );
This would be "fetching" a list of posts and then clicking on the post and having an individual item fetched.
import React from "react"; import { useQuery } from "react-query"; import { ReactQueryDevtools } from "react-query-devtools"; import axios from "axios"; function Posts({ setPostId }) { const postsQuery = useQuery("posts", async () => { await new Promise((resolve) => setTimeout(resolve, 1000)); return axios .get("https://jsonplaceholder.typicode.com/posts") .then((res) => res.data); }); return ( <div> <h1>Posts {postsQuery.isFetching ? "..." : null}</h1> <div> {postsQuery.isLoading ? ( "Loading posts..." ) : ( <ul> {postsQuery.data.map((post) => { return ( <li key={post.id}> <a onClick={() => setPostId(post.id)} href="#"> {post.title} </a> </li> ); })} </ul> )} </div> </div> ); } function Post({ postId, setPostId }) { const postQuery = useQuery(["post", postId], async () => { await new Promise((resolve) => setTimeout(resolve, 1000)); return axios .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) .then((res) => res.data); }); return ( <div> <a onClick={() => setPostId(-1)} href="#"> Back </a> <br /> <br /> {postQuery.isLoading ? ( "Loading..." ) : ( <> {postQuery.data.title} <br /> <br /> {postQuery.isFetching ? "Updating..." : null} </> )} </div> ); // } export default function App() { const [postId, setPostId] = React.useState(-1); return ( <div> {postId > -1 ? ( <Post postId={postId} setPostId={setPostId} /> ) : ( <Posts setPostId={setPostId} /> )} <ReactQueryDevtools /> </div> ); }
import { useQuery, queryCache } from "react-query"; // ... code omitted const postQuery = useQuery( ["post", postId], async () => { await new Promise((resolve) => setTimeout(resolve, 1000)); return axios .get(`https://jsonplaceholder.typicode.com/posts/${postId}`) .then((res) => res.data); }, { initialData: () => queryCache.getQueryData("posts")?.find((post) => post.id === postId), initialStale: true, } );
To use more of a "push" mentality to preset the queries:
const postsQuery = useQuery('posts', async () => { await new Promise(resolve => setTimeout(resolve, 1000)) const posts = await axios .get('https://jsonplaceholder.typicode.com/posts') .then(res => res.data) posts.forEach(post => { queryCache.setQueryData(['post', post.id], post) }) return posts })
This "pushes" the data from a larger fetch to preset the data for checking individual Posts later based on the key.
It is worth noting that setQueryData
is consideres stale immediately.
There are callback functions that we can add to create side-effects. This could also be quite useful for state management.
const [count, increment] = React.useReducer(d => d + 1, 0) const postsQuery = useQuery('posts', fetchPosts, { onSuccess: data => { increment() }, onError: error => {}, onSettled: (data, error) => {}, })
Note that this happens per-instance. Mutiple calls to this useQuery
will result in multiple onSuccess
calls.
Thanks to the query cache, we have out-of-the-box scroll restoration to bring us back to where we were on a particular page.
If you do set the cacheTime
and the query cache is garbage collected, then you'll notice that you are not brought down to the particular page that we want.
We can use refetchInterval
and refetchIntervalInBackground
to poll.
import React from 'react' import { useQuery } from 'react-query' import axios from 'axios' export default function Posts() { const timeQuery = useQuery( 'posts', async () => { return axios.get('/api/time').then(res => res.data) }, { refetchInterval: 1000, refetchIntervalInBackground: true, } ) return ( <div> <h1>Server Time {timeQuery.isFetching ? '...' : null}</h1> <div> {timeQuery.isLoading ? 'Loading time...' : new Date(timeQuery.data.time).toLocaleString()} </div> </div> ) }
refetchIntervalInBackground
is not a great idea if you don't want to keep polling while on other tabs etc.