https://daily.dev/blog/usequery-react-for-efficient-data-fetching
If you're diving into React and looking to manage data fetching efficiently, useQuery
from React Query is a game-changer. It simplifies data fetching, caching, and synchronization, making your app responsive and up-to-date. Here's a quick rundown:
- Easy Data Fetching: Simplifies fetching data with a single hook.
- Automatic Caching: Saves fetched data to reduce load times and server requests.
- Background Updates: Keeps your app's data fresh without user intervention.
- Error Handling: Smoothly manages errors and retries fetching if needed.
- Developer Tools: Offers tools for debugging and optimizing your data fetching strategies.
To get started, install react-query, set up a QueryClient, and wrap your app with QueryClientProvider. Use useQuery
in your components with a unique key and a fetch function. The hook handles the rest, from loading states to caching and updating the data.
Whether you're fetching a todo list from an API or implementing a dynamic search feature, useQuery
handles the heavy lifting, letting you focus on building a great user experience. It's perfect for apps that need real-time data without the hassle of manual data management.
Understanding useQuery Hook Parameters and Return Values
Key Parameters
The useQuery
hook needs two main things to work:
- queryKey (required): Think of this as a unique label for your query. It can be a simple string or an array. This label helps the system remember and reuse your query efficiently.
- queryFn (required): This is the function that actually goes and gets your data. It should be an async function that fetches data and returns it.
For instance:
useQuery(['users', 5], async () => {
const res = await fetch('/api/users/5');
return res.json();
});
In this example, ['users', 5]
is the unique label, and the function fetches data for user ID 5.
Configuration Options
You can also tweak how useQuery
works with a few options:
- cacheTime: How long to keep data fresh even if it's not being used. Helps to not fetch data too often.
- staleTime: How long before the fetched data is considered old. If it's old, it might fetch it again in the background.
- refetchOnWindowFocus: Whether to get fresh data automatically when you come back to the window. Keeps data up-to-date.
Example:
useQuery('users', fetchUsers, {
cacheTime: 10000,
staleTime: 30000,
refetchOnWindowFocus: true
})
Return Values
useQuery
gives back an object with helpful info:
- status: Tells you if it's loading, if there was an error, or if it's successful
- error: Any errors that happened
- data: The data you fetched
- isFetching: True if it's currently fetching data
Example:
const { status, data, error, isFetching } = useQuery('users', fetchUsers);
if (status === 'loading') {
return <Spinner />;
}
if (status === 'error') {
return <Message error={error} />;
}
return <UserList data={data} />;
This setup lets you manage loading screens, errors, and how to show your data in a simple way.
Implementing Core Data Fetching Use Cases
Fetching Todos from API
Here's a simple way to get a list of tasks (todos) from an online service using useQuery
in your React app. This example also shows how to deal with waiting for the data to load and handling any errors that might pop up:
import { useQuery } from 'react-query';
import axios from 'axios';
function Todos() {
const fetchTodos = () => axios.get('/api/todos');
const {
isLoading,
error,
data: todos
} = useQuery('todos', fetchTodos);
if (isLoading) {
return <p>Loading...</p>;
}
if (error) {
return <p>An error occurred: {error.message}</p>;
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
This code neatly covers getting the data, showing a message while waiting, and dealing with errors. Once the data is fetched, it's ready to be shown.
Dynamic Search Implementation
This example shows how to set up a search feature in your React app where useQuery
automatically looks for results based on what the user types:
import { useState } from 'react';
import { useQuery } from 'react-query';
import axios from 'axios';
function Search() {
const [searchTerm, setSearchTerm] = useState('');
const {
data: results,
isLoading,
error
} = useQuery(
['search', searchTerm],
() => axios.get(`/api/search?query=${searchTerm}`),
{ enabled: !!searchTerm }
);
return (
<>
<input
type="text"
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="Search"
/>
{isLoading && <p>Loading...</p>}
{error && <p>Something went wrong: {error.message}</p>}
<ul>
{results.map(result => (
<li key={result.id}>{result.name}</li>
))}
</ul>
</>
);
}
In this setup, the search starts only when there's something typed in. The query updates and fetches new results whenever the search term changes.
Advanced Capabilities
Caching and Background Refetching
useQuery
is really good at remembering data it has fetched before. So, if you ask for something it has already gotten, like a list of items, it won’t ask the server again but will give you the saved version. This makes your app quick for users.
But, if you always want the latest info, useQuery
offers some cool features:
refetchInterval
: This tells useQuery
to get fresh data every few seconds.refetchOnWindowFocus
: This makes useQuery
get new data when someone comes back to the app after looking at something else.
You can also ask for new data whenever you want with refetch()
.
Here’s how you can set it to get new data every five minutes:
const { data } = useQuery('products', fetchProducts, {
cacheTime: 1000 * 60 * 60,
refetchInterval: 1000 * 60 * 5,
})
The cool thing is, useQuery
does all this without making your app slow or showing loading screens all the time.
Your app stays quick and keeps data up to date without users noticing much.
Implementing Paginated Data Fetching
When you need to show data in chunks, like a few items at a time, useQuery
can help. Here’s a simple way to do it:
function Results({ page }) {
const { resolvedData, latestData, status } = useQuery(
['results', page],
() => fetch(`/results?page=${page}`).then(res => res.json()),
{ keepPreviousData: true }
)
if (status === 'loading') {
return <Spinner />
}
if (status === 'error') {
return <ErrorMessage />
}
return (
<div>
{/* If new data is there, show it, otherwise show old data */}
{latestData?.results || resolvedData?.results}
<button onClick={() => setPage(page + 1)}>
Next Page
</button>
</div>
)
}
What to remember:
- Your page number is part of the query key.
keepPreviousData
lets you keep seeing the old data until the new data is ready.- You can switch between the latest and the previously fetched data.
This makes handling pages simple without needing to juggle too much code.
Logging Query States
Another way to figure out what's going wrong is by adding logs to your queries. Here's an example:
useQuery('todos', fetchTodos, {
onError: (err) => console.log(err),
onSettled: () => console.log('Query settled'),
})
onError
will print out errors if something goes wrong.onSettled
tells you when the query has finished, regardless of whether it was successful or not.- You can also add logs in your components to see what's happening when data is loading or when there's an error.
Adding these logs helps you see exactly where things are going off track, without making your code complicated.