Getting started
Handling state, which comes from the server can really cause some headaches in React. There is a lot you have to think about when dealing with asynchronous data, like updating, caching, or re-fetching to name a few.
This is where react-query comes in. It handles those smoothly and also offers simple solutions for optimistic rendering, infinite scroll, pagination, and more.
Here's a small demo of what we'll be building:
If you'd like to jump straight into the code, you can find the repository here:
https://github.com/wwebdev/react-query-demo
For this tutorial, I'll assume you have node installed. First of all, create a new react app with npx create-react-app
. Afterward, install react-query with npm i --save react-query
For demonstrating how react-query works I'll use the Json Placeholder API to create a simple blog.
Fetching the data
First of all, I'll remove all the boilerplate code in the App.js and replace it with the following code:
import React from 'react';
import { useQuery } from 'react-query'
const getPosts = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
return response.json()
}
function App() {
const { status, data, isFetching, error } = useQuery('posts', getPosts)
if (status === 'loading') {
return <div>loading...</div> // loading state
}
if (status === 'error') {
return <div>{error.message}</div> // error state
}
return (
<div>
{ data && <ul>{
data
.slice(0,10) // only take frist 10 for now
// render list of titles
.map(d => <li key={`post-${d.id}`}>{d.title}</li>)
}</ul> }
{ isFetching && <p>updating...</p> }
</div>
)
}
export default App
First I define a function called getPosts
- this can include anything as long as it's returning an asynchronous function.
At the beginning of the App()
the useQuery hook is called with an identifier for the data that's being fetched as well as out async function getPosts.
The hook returns status, data, isFetching, and error. Those are pretty self-explanatory. The status can be either "success", "loading" or "error". The rest of the component handles the display of the result on the three possible states.
The internals of react-query will now take care of all the caching and updating logic. This means whenever you go to this page you'll know that the displayed data will be there instantly if you've previously fetched it and it will be always up to date with the server state.
That's actually all you need to know to get started using react-query. But let's extend this example to see this caching and updating in action!
Extending the application
First of all, I'll move the code from App.js to a new component components/Home.js
. Therefore I will rename the component and I'll also add a NavLink
to the post list.
import React from 'react'
import { NavLink } from 'react-router-dom'
import { useQuery } from 'react-query'
const getPosts = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
await new Promise(r => setTimeout(r, 1000)) // wait a second
return response.json()
};
function Home() {
const { status, data, isFetching, error } = useQuery('posts', getPosts)
if (status === 'loading') {
return <div>loading...</div> // loading state
}
if (status === 'error') {
return <div>{error.message}</div> // error state
}
return (
<div>
{ data && <ul>{
data
.slice(0,10) // only take frist 10 for now
.map(d => <li key={`post-${d.id}`}>
<NavLink to={`/post/${d.id}`}>{d.title}</NavLink>
</li>) // render list of titles
}</ul> }
{ isFetching && <p>updating...</p> }
</div>
);
}
export default Home
Now let's add a router to App.js, which accepts the routes to /
for Home.js and /post/:id
for a single post page.
import React from 'react'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import Home from './components/Home'
import Post from './components/Post'
function App() {
return (
<Router>
<Switch>
<Route exact path="/" component={Home}/>
<Route path = '/post/:id' render = {routerProps => <Post id={routerProps.match.params.id}/>} />
</Switch>
</Router>
)
}
export default App
And finally, I'll create a new component components/Post.js
for displaying the data of a single post. The explanation will follow after the code.
import React from 'react'
import { NavLink } from 'react-router-dom'
import { useQuery } from 'react-query'
const Post = ({ id }) => {
const getPost = async () => {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
const jsonResponse = await response.json()
jsonResponse.title = `${jsonResponse.title} - ${Math.random().toString(36)}`
await new Promise(r => setTimeout(r, 1000)) // wait a second
return jsonResponse
}
const { status, data, isFetching } = useQuery(`post-${id}`, getPost)
if (status === 'loading') {
return <div>loading...</div> // loading state
}
return (
<div>
<h1>{data.title}</h1>
<p>{data.body}</p>
{ isFetching && <p>updating...</p> }
<br />
<NavLink to="/">Home</NavLink>
</div>
)
}
export default Post
So the useQuery
in here doesn't differ much from the one in Home.js. It adds the id
to the identifier, so each post has its own state. Additionally, I've added a timeout for 1 second to the getPost
function to make the loading state more visible. Also, I've appended a random string to the title to make the re-fetching visible.
And that's actually the whole code for the gif you saw at the beginning of the post.
If you start working with react-query, I'd recommend you to check out the react-query-devtools, to be able to view the state and the cache.
Feel free to check out the code on GitHub. Also let me know if you'd like to know more about the usage of react-query for things like initial data
, pre-fetching
, optimistic rendering
and I'll extend this to a series of posts.