React query provides hooks for managing server data in react application.
Imagine you are creating an application that receives data from an API and displays that data. How will you handle that data? State management yes!
With normal flow you will have to write logic to fetch data, write reducers, caching logic, retry (it could be more… depending on the application).
You will be surprised with the number of codes you will need using react query.
Among-st many things, react-query offers server data management, caching, re-fetching, mutation API. Learn more.
To get a glimpse of react query lets create a simple blog application using react query and react.
Install react-query
and axios
in your react app.
npm install react-query axios
Then wrap your entire provider in react query provider
in index.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import { QueryClient, QueryClientProvider } from "react-query";
import App from "./App";
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);
Lets do the fetching first
in App.jsx
import useQuery
from react-query
import { useQuery } from "react-query";
Create a first api request to fetch sample blog posts.
import { useQuery } from "react-query";
const App = () => {
const { data, isLoading, error } = useQuery({
queryKey: ["FETCH_POSTS"],
queryFn: async () => {
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/posts"
);
return data;
},
});
console.log(data);
return <div className="App">Hello world</div>;
};
export const App
NB: useQuery is a hook and can only be called inside the react function component
from the simple example above, data will hold any data success response from the api call, error will hold error data, isLoading will hold the loading status (true/ false). The returned data will be cached after initial success request, whenever you call the same api via use query in another component, the data will persist that you won't need to store them in any state.
QueryKey keeps track of the memory required to handle data caching, When useQuery is used, it checks the cache first using query key and if none is found, then it makes actual API request, if cache is present, it returns the existing data. Its can be array of a simple string to array of complex data depending on application complexity.
Lets display the fetched data and allow user to view single post.
const App = () => {
return (
<div className="App">
{/* display data */}
<Posts />
</div>
);
};
// all posts
const Posts = () => {
const { data, isLoading, error, status } = useQuery({
queryKey: ["FETCH_POSTS"],
queryFn: async () => {
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/posts"
);
return data;
},
});
return (
<div>
{isLoading && <div>Please wait...</div>}
{data &&
data.map((post) => (
<PostCard
key={post.id}
post={post}
/>
))}
</div>
);
};
// Post Card
const PostCard = ({ post }) => {
return (
<a href="#">
<p>{post.title}</p>
</a>
);
};
This will display all the data from the, lets set a way to view single post in the same screen. React router can be used but for this case we can use state to render all posts and single post on post clicking.
const App = () => {
const [selectedPost, setSelectedPost] = useState(-1);
return (
<div className="App">
{/* display data */}
{selectedPost === -1 && <Posts selectPost={(id) => setSelectedPost(id)} />}
{/* add a back button on viewing single post */}
{selectedPost && (
<a href="#" onClick={() => setSelectedPost(-1)}>
Back
</a>
)}
{/* viewing single post details */}
{selectedPost > -1 && <Post id={selectedPost} />}
</div>
);
};
// all posts
const Posts = ({selectPost) => {
const { data, isLoading, error, status } = useQuery({
queryKey: ["FETCH_POSTS"],
queryFn: async () => {
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/posts"
);
return data;
},
});
return (
<div>
{isLoading && <div>Please wait...</div>}
{data &&
data.map((post) => (
<PostCard
key={post.id}
post={post}
onClick={() => selectPost(post.id)}
/>
))}
</div>
);
};
// Post Card
const PostCard = ({ post, onClick }) => {
return (
<a href="#" onClick={onClick}>
<p>{post.title}</p>
</a>
);
};
const Post = ({ id }) => {
return <div>Hello post</div>;
};
Now we should be able to click and view a single post and go back to viewing all posts.
Lets fetch data on a <Post />
component.
const Post = ({ id }) => {
const { data, isLoading, error, status } = useQuery({
queryKey: ["FETCH_POST", id],
queryFn: async () => {
const { data } = await axios.get(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
return data;
},
});
return (
<div>
{isLoading && <div>Fetching post...</div>}
{data && (
<div>
<h3>{data.title}</h3>
<p>{data.body}</p>
</div>
)}
</div>
);
};
Note that in query key, We added id, id is unique for each post and helps to keep memory in the cache for that particular post we just viewed, clicking the same post again won't make an API call, instead will return the cached data.
For a case where we don't need cache, and every time user visits the component or page, we want to make an actual request, we can prevent cache by adding an cacheTime as an option in useQuery params.
const Post = ({ id }) => {
const { data, isLoading, error, status } = useQuery({
queryKey: ["FETCH_POST", id],
queryFn: async () => {
const { data } = await axios.get(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
return data;
},
cacheTime: 0,
});
return (
<div>
{isLoading && <div>Fetching post...</div>}
{data && (
<div>
<h3>{data.title}</h3>
<p>{data.body}</p>
</div>
)}
</div>
);
};
This will run every time a post is visited, even the one which was already visited before.
Another cool thing we can do is adding refetch button. Assume you visit a page which returns an error that data fetching failed, you have no choice but to refresh the page. Well you can add a refetch button and use refetch provided by react query out of the box, lets see how we can utilize that
const Post = ({ id }) => {
const { data, isLoading, error, status, isRefetching, refetch } = useQuery({
queryKey: ["FETCH_POST", id],
queryFn: async () => {
const { data } = await axios.get(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
return data;
},
cacheTime: 0,
});
return (
<div>
{(isLoading || isRefetching) && <div>Fetching post...</div>}
{data && (
<div>
<h3>{data.title}</h3>
<p>{data.body}</p>
</div>
)}
{error && <p>{error.message}</p>}
{error && <button onClick={refetch}>Refetch</button>}
</div>
);
};
We can also enable or disable data fetching using enable
option. When the query is disabled no fetching will be done until it is enabled, this feature can be used when implementing lazy query (queries that are triggered by user action.
Lets see an example of this. In the post component, lets say we want user to trigger initial fetching by clicking something.
const Post = ({ id }) => {
const [enabled, setEnabled] = useState(false);
const { data, isLoading, error, status, isRefetching, refetch } = useQuery({
queryKey: ["FETCH_POST", id],
queryFn: async () => {
const { data } = await axios.get(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
return data;
},
cacheTime: 0,
enabled,
});
return (
<div>
{!data && !error && (
<button onClick={() => setEnabled(true)}>Fetch Post</button>
)}
{(isLoading || isRefetching) && <div>Fetching post...</div>}
{data && (
<div>
<h3>{data.title}</h3>
<p>{data.body}</p>
</div>
)}
{error && <p>{error.message}</p>}
{error && <button onClick={refetch}>Refetch</button>}
</div>
);
};
Now every time user clicks to view a single post, they will have to click Fetch button to load post details. This is useful especially on search screen where you only allow fetching when user search something.
There are more options on useQuery hook that might help in your project such as refetchOnWindowFocus
, refetchOnMount
(By default these two are true).
Other options include refetchInterval
specifies at with interval you want to be pooling your data (in milliseconds).
React-query works best with pagination as well as fetching and re-fetching data on background.
React query provides useMutation
hook that provides data creation, edition and deletion. In my next post we'll cover how to mutate data with useMutation hook.
If you are using GraphQL API, the best alternative to react-query is Apollo Client although react-query can also be used with GraphQL.
If you like this article there are more like this in our blogs, follow us on dev.to/clickpesa, medium.com/clickpesa-engineering-blog and clickpesa.hashnode.dev
Happy Hacking!!