Introduction
Paint this: You created a web application where you must navigate to different pages, and each page fetches some data from the server. If you are not prefetching the data and navigating to different pages, it will send a GET request each time to get the data, leading to a delay and not the best user experience.
This is where prefetching comes into play.
What is Prefetching in TanStack Query?
Prefetching is the act of fetching data before we actually need it. By prefetching, you ensure that wherever you need data, it is already at your disposal without making unnecessary requests, leading to a smooth user experience.
To prefetch data in the React application with TanStack Query, use the queryClient.prefetchQuery() function.
What is Pruning in TanStack Query?
Pruning is the act of removing the unnecessary data from the cache. This process helps us manage the cache and occasionally clean it to ensure that the application does not hold onto stale or unnecessary data, which can lead to memory bloat.
Prefetching and Pruning can enhance your application’s performance by fetching data early, storing it in the cache, and removing unnecessary data from the cache when the application does not require it.
Project Overview
In this post, we will create a simple React web application that prefetches and displays data from an API. We also prune the data by setting staleTime and cacheTime options. After the specified time, the data from the cache will be pruned.
Here is the step-by-step guide to create a small web application where we showcase how to prefetch the data and prune it when it becomes irrelevant.
Step 1: Setup a React project with Vite bundler
Let’s set up a new Vite project with a React template using the below command:
npm create vite@latest prefetch-app -- --template react cd prefetch-app
Install the necessary dependencies using the command below:
npm install
Now, install the packages related to MUI, Axios, and TanStack Query using the below command:
npm install axios @mui/material @emotion/react @emotion/styled @tanstack/react-query
Step 2: Create layout and navbar components
Under the “src” folder, create a new folder called “components”, and inside that folder, create a new file called “Navbar.jsx” and add the code below:
// src/components/Navbar.jsx import { AppBar, Toolbar, Typography } from '@mui/material'; const Navbar = () => ( <AppBar position="static"> <Toolbar> <Typography variant="h6"> My Blog </Typography> </Toolbar> </AppBar> ); export default Navbar;
This component displays the navigation bar in our application using MUI.
Now, create a layout file for our application that also includes Navbar.
Under the components folder, create a new file called Layout.jsx and add the code below:
// src/components/Layout.jsx import { Container, CssBaseline } from '@mui/material'; import Navbar from './Navbar'; const Layout = ({ children }) => ( <> <CssBaseline /> <Navbar /> <Container> {children} </Container> </> ); export default Layout;
Step 3: Create custom hooks
Our main aim for this tutorial is to prefetch the data. How do we prefetch the data in TanStack Query (React Query)? Use the queryClient.prefetchQuery() hook.
We will create three custom hooks, each with its purpose.
- usePosts.js
- usePrefetchPost.js
- usePostDetails.js
The usePosts hook is responsible for sending a GET request using axios.get() function to an API to fetch the data. We will define two functions to fetch two different types of data.
Under the “src” folder, create a new folder called “hooks”, and in that folder, create a new file called usePosts.js and add the code below:
// src/hooks/usePosts.jsx import axios from 'axios'; export const fetchPosts = async () => { const { data } = await axios.get('https://jsonplaceholder.typicode.com/posts'); return data; }; export const fetchPost = async (id) => { const { data } = await axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`); return data; };
The second custom hook would be called usePrefetchPost, which will prefetch the data using the queryClient.prefetchQuery function.
Under the “hooks” folder, create a new file called usePrefetchPost.js and add the code below:
// src/hooks/usePrefetchPost.js import { useQueryClient } from '@tanstack/react-query'; import { fetchPost } from './usePosts'; const usePrefetchPost = () => { const queryClient = useQueryClient(); const prefetchPost = (id) => { queryClient.prefetchQuery({ queryKey: ['post', id], queryFn: () => fetchPost(id), staleTime: 5 * 60 * 1000, // 5 minutes }); }; return prefetchPost; }; export default usePrefetchPost;
The TanStack React Query’s queryClient.prefetchQuery() function prefetches data for a query without immediately triggering a component re-render. This improves performance because we load the data in advance, making our application smooth and user-friendly.
It typically prefetches the data by hovering over a link or navigating through a list.
The prefetch() function accepts an object that has the following properties:
- queryKey: It is a unique key that identifies the query. We can use this key to fetch the posts anywhere in our application.
- queryFn: It is an async function that performs a side-effect and fetches the actual data from the server.
- staleTime: The time in milliseconds for which the prefetched data is considered fresh. After this time, the data becomes stale and no longer helpful. We might prune it since it will waste our memory.
Our third and last custom hook is to fetch the specific post details. Create a new file usePostDetails.js under the “hooks” folder and add the code below:
// src/hooks/usePostDetails.js import { useQuery } from '@tanstack/react-query'; import { fetchPost } from './usePosts'; const usePostDetails = (postId) => { return useQuery({ queryKey: ['post', postId], queryFn: () => fetchPost(postId), enabled: !!postId, }); }; export default usePostDetails;
We need to fetch the post based on a specific id. That’s why we passed the enabled property in the useQuery hook. So, each hook is responsible for interacting with API data and logic.
Step 4: Create a context
Since our application does not use any type of router, another way to display a specific post is to use React Context.
A context provides a way to pass data through the component tree without manually passing props down at every level. React provides the useContext hook that allows us to use the context value in functional components.
Under the “src” folder, create a new folder called “context”.
Under the “context” folder, create a file called PostContext.jsx. This file is the post-related context, meaning you can use it to provide or consume post content.
// src/context/PostContext.jsx import { createContext, useContext, useState } from 'react'; const PostContext = createContext(); export const usePostContext = () => useContext(PostContext); export const PostProvider = ({ children }) => { const [postId, setPostId] = useState(null); return ( <PostContext.Provider value={{ postId, setPostId }}> {children} </PostContext.Provider> ); };
We created a PostContext using createContext() method. Then, pass this PostContext to the useContext hook.
The PostProvider is a provider component that accepts children as an argument, which means it will act as a parent component. It wraps children with PostContext.Provider, making the postId and setPostId available to all descendant components.
Step 5: Create Post related components
Under the “components” folder, create a new file called PostList.jsx and add the code below:
// src/components/PostList.jsx import { useQuery } from '@tanstack/react-query'; import { List, ListItemButton, ListItemText, CircularProgress, Typography } from '@mui/material'; import { usePostContext } from '../context/PostContext'; import usePrefetchPost from '../hooks/usePrefetchPost'; import { fetchPosts } from '../hooks/usePosts'; const PostList = () => { const { setPostId } = usePostContext(); const prefetchPost = usePrefetchPost(); const { data, error, isLoading } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts }); if (isLoading) return <CircularProgress />; if (error) return <Typography color="error">An error occurred: {error.message}</Typography>; const handleMouseEnter = (id) => { if (typeof prefetchPost === 'function') { prefetchPost(id); } else { console.error('prefetchPost is not a function', prefetchPost); } }; return ( <List> {data.map(post => ( <ListItemButton key={post.id} onClick={() => setPostId(post.id)} onMouseEnter={() => handleMouseEnter(post.id)} > <ListItemText primary={post.title} /> </ListItemButton> ))} </List> ); }; export default PostList;
Now, create another component file. Under the “components” folder, create a new file called PostDetails.jsx and add the code below:
// src/components/PostDetails.jsx import { CircularProgress, Typography } from '@mui/material'; import { usePostContext } from '../context/PostContext'; import usePostDetails from '../hooks/usePostDetails'; const PostDetails = () => { const { postId } = usePostContext(); const { data, error, isLoading } = usePostDetails(postId); if (!postId) return <Typography>Select a post to view details</Typography>; if (isLoading) return <CircularProgress />; if (error) return <Typography color="error">An error occurred: {error.message}</Typography>; return ( <> <Typography variant="h4" component="h1" gutterBottom> {data.title} </Typography> <Typography variant="body1">{data.body}</Typography> </> ); }; export default PostDetails;
Step 6: Update the App.jsx and main.jsx files
Update the src/App.jsx file like this:
// src/App.jsx import Layout from './components/Layout'; import PostList from './components/PostList'; import PostDetails from './components/PostDetails'; import { PostProvider } from './context/PostContext'; const App = () => ( <PostProvider> <Layout> <PostList /> <PostDetails /> </Layout> </PostProvider> ); export default App;
Update the src/main.jsx file like this:
// src/main.jsx import React from 'react'; import ReactDOM from 'react-dom/client'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import App from './App'; export const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, // 5 minutes cacheTime: 10 * 60 * 1000, // 10 minutes }, }, }); ReactDOM.createRoot(document.getElementById('root')).render( <React.StrictMode> <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider> </React.StrictMode> );
In this code, we are configuring QueryClient with staleTime and cacheTime options:
- staleTime: It specifies how long a query’s data is considered fresh. Here, data is fresh for 5 minutes.
- cacheTime: It specifies how long unused/inactive query data remains in the cache. Here, it’s set to 10 minutes.
This configuration ensures that the application prefetches data efficiently and prunes stale data from the cache, maintaining optimal performance and memory usage.
Step 7: Run the project
Start the Vite development server using the command below:
npm run dev
Go to this URL: http://localhost:5173/
If you click on the mentioned post as shown in the figure, you will see the post detail like this image:
After 10 mins, the data from the cache will be pruned.
That’s all for pruning and refetching the data in React.