Data Fetching in React with useQuery

Jovial Blog
5 min readAug 28, 2024

--

Introduction

Managing data fetching in React can be a challenge. If you’ve ever struggled with complex state management, performance issues, or repetitive code, you’re not alone. These are common pain points that many developers face when building React applications. But what if there was a way to simplify this process, make your code more maintainable, and enhance the overall performance of your app?

Enter useQuery—a powerful hook from the React Query library that can transform how you handle data fetching in React. In this blog, you’ll learn exactly what useQuery is, how it works, and how you can use it to streamline your data-fetching process. We’ll walk through a real-world example, address common challenges, and explore some advanced features that can take your React applications to the next level.

By the end of this article, you’ll have a solid understanding of how useQuery can make data fetching more efficient, reliable, and easier to manage in your React projects.

What is useQuery?

The useQuery hook is a part of the React Query library, designed to handle server-state in React applications. Server-state refers to data that resides outside of your React application but is essential for rendering components—think of data fetched from an API or a database.

Traditionally, React developers have relied on useEffect combined with fetch or axios to retrieve data. While this approach works, it often leads to repetitive code, complex state management, and challenges with caching and synchronization. useQuery simplifies this by abstracting away the complexity and offering a declarative way to fetch, cache, and sync data within your React components.

Here’s a quick comparison:

Traditional Data Fetching with useEffect

import React, { useEffect, useState } from 'react';

function MyComponent() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
setError(error);
setLoading(false);
});
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <div>{data}</div>;
}

Data Fetching with useQuery

import React from 'react';
import { useQuery } from 'react-query';

function MyComponent() {
const { data, error, isLoading } = useQuery('fetchData', () =>
fetch('https://api.example.com/data').then(res => res.json())
);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <div>{data}</div>;
}
}

As you can see, useQuery significantly reduces the amount of boilerplate code, making your components cleaner and easier to maintain. But that’s just the beginning—let’s dive into a real-world example to see useQuery in action.

Blogging Example: Implementing useQuery in a React Application

Imagine you’re building a blog application where you need to fetch and display a list of posts from an API. Using useQuery, you can handle this task with minimal setup and maximum efficiency.

First, you need to install React Query in your project:

npm install react-query

Next, wrap your application in the QueryClientProvider to provide the necessary context:

import React from 'react';
import ReactDOM from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import App from './App';

const queryClient = new QueryClient();
ReactDOM.render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>,
document.getElementById('root')
);

Now, let’s fetch the list of posts:

import React from 'react';
import { useQuery } from 'react-query';

function PostsList() {
const { data, error, isLoading } = useQuery('posts', () =>
fetch('https://api.example.com/posts').then(res => res.json())
);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
export default PostsList;

One of the key benefits of useQuery is its built-in caching mechanism. By default, React Query caches the data and reuses it across your components. It also handles stale data automatically, ensuring that your app is always showing the most up-to-date information.

For example, you can configure the stale time like this:

const { data, error, isLoading } = useQuery('posts', fetchPosts, {
staleTime: 5000, // Data will be considered fresh for 5 seconds
});

This means the data will be considered fresh for 5 seconds, and React Query won’t refetch it unless explicitly instructed.

Challenges and Solutions with useQuery

Challenge 1: Handling Errors Gracefully

One common challenge is managing errors effectively. useQuery provides a straightforward way to handle errors, as seen in the example above. However, for more complex scenarios, you might want to add custom error handling logic.

const { data, error, isLoading } = useQuery('posts', fetchPosts, {
onError: (error) => {
// Custom error handling logic
console.error('Error fetching posts:', error);
},
});

Challenge 2: Synchronising Data Across Components

When multiple components depend on the same data, keeping them in sync can be tricky. useQuery solves this by sharing the query cache across components. As long as the query key ('posts' in this case) is the same, all components will use the same cached data, ensuring consistency.

Advanced Features of useQuery

Pagination and Infinite Scrolling

React Query makes it easy to implement pagination or infinite scrolling. You can pass parameters to your query function and manage the state of the current page:

const fetchPosts = async (page) => {
const res = await fetch(`https://api.example.com/posts?page=${page}`);
return res.json();
};

const { data, error, isLoading, fetchNextPage, hasNextPage } = useInfiniteQuery(
'posts',
({ pageParam = 1 }) => fetchPosts(pageParam),
{
getNextPageParam: (lastPage, pages) => {
return lastPage.nextPage; // Assuming your API returns the next page number
},
}
);

Optimistic Updates

Optimistic updates allow you to update the UI immediately, even before the server confirms the change. This enhances the user experience by making your app feel faster and more responsive.

const mutation = useMutation(updatePost, {
onMutate: (newData) => {
queryClient.setQueryData('posts', (oldData) => {
// Update the cached data optimistically
return oldData.map(post => post.id === newData.id ? newData : post);
});
},
onError: (error, newData, context) => {
// Rollback the changes if the mutation fails
queryClient.setQueryData('posts', context.previousData);
},
onSettled: () => {
queryClient.invalidateQueries('posts'); // Refetch after mutation
},
});

Conclusion

The useQuery hook is a game-changer for managing data fetching in React applications. By simplifying the process and providing powerful features like caching, pagination, and optimistic updates, useQuery helps you write cleaner, more maintainable code while improving your app’s performance.

Whether you’re dealing with complex data fetching requirements or just want to make your React components more efficient, useQuery offers a reliable, scalable solution. Start integrating useQuery into your projects today, and experience the difference it can make :)

--

--

Jovial Blog
Jovial Blog

Written by Jovial Blog

Software Developer & Engineer

No responses yet