React Query: Making Your Server-State Problems Disappear (Like Magic)
Sathish
Posted on September 9, 2024
Managing server state in React is like trying to juggle flaming swords—possible, but unnecessarily difficult. You could wrestle with useEffect
and useState
to handle data fetching, caching, and synchronization, but that approach often leads to bloated components and messy code. Enter React Query, the superhero you didn’t know you needed! It’s like your personal assistant for managing server state—fetching, caching, and syncing all while sipping coffee.
In this blog post, we’ll explore what React Query is, how it works, and why it makes managing server-state easy (and fun!). We’ll also cover the magical powers of cache invalidation and provide some code examples to make it all click.
What is React Query?
React Query is a data-fetching and state management library for React that abstracts away all the headache-inducing aspects of working with asynchronous data. It comes with out-of-the-box features like caching, background updates, and synchronization between tabs. Basically, it’s like giving your React app superpowers.
It helps you focus on building features rather than obsessing over when or how your data will be fetched. It works great with REST APIs, GraphQL, or any other asynchronous data source.
Why Should You Care?
If you're tired of writing complex logic to handle server state, handling loading states, refetching data, or even canceling network requests, then React Query is your solution. It makes managing data as simple as calling a function and keeps the rest of your app reactive, fast, and performant.
Let’s Get Our Hands Dirty: Installing React Query
First things first, let’s add React Query to your project:
npm install react-query
Next, wrap your app in a QueryClientProvider
—this is the magical sauce that makes everything work.
import { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourAwesomeApp />
</QueryClientProvider>
);
}
export default App;
Now you’re ready to start querying data like a pro!
Basic Usage: Fetching Data the Easy Way
Let’s start with the simplest example: fetching data with React Query. Instead of painstakingly managing your state and side effects, you’ll just use the useQuery
hook.
import { useQuery } from 'react-query';
import axios from 'axios';
const fetchTodos = async () => {
const { data } = await axios.get('/api/todos');
return data;
};
function Todos() {
const { data, error, isLoading } = useQuery('todos', fetchTodos);
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Oh no, something went wrong!</p>;
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
Here’s what’s happening:
- The
useQuery
hook takes two arguments: a unique key ('todos'
) and a function that fetches the data (fetchTodos
). - React Query automatically handles loading states, caching, and error handling for you, so you can focus on what matters: building your app.
The Cache: React Query’s Hidden Superpower
The coolest feature of React Query is its built-in caching. When data is fetched with useQuery
, it’s stored in the cache for future use. This means that subsequent requests for the same data will be served instantly from the cache (no need to hit the server again unless necessary).
Here’s how it works:
- Stale-While-Revalidate: By default, React Query treats cached data as stale, but usable. It will immediately return the stale data while re-fetching in the background.
- Automatic Garbage Collection: Data in the cache will stick around until it’s unused for a certain period (default is 5 minutes), after which it gets cleaned up to save memory.
For instance, if you switch between tabs that both use the same data, you won’t be hitting the server again and again—it just serves you the cached version until the background refetch finishes.
Cache Invalidation: Making Sure Your Data Is Fresh as a Daisy
Ah, cache invalidation—the classic computer science problem! Luckily, React Query makes it much easier to handle.
When you mutate data (for instance, by adding a new todo), you need to tell React Query to invalidate the cache so it knows to refetch the data. You can do this using the useMutation
hook and queryClient.invalidateQueries
function.
import { useMutation, useQueryClient } from 'react-query';
import axios from 'axios';
const addTodo = async (newTodo) => {
const { data } = await axios.post('/api/todos', newTodo);
return data;
};
function AddTodo() {
const queryClient = useQueryClient();
const mutation = useMutation(addTodo, {
onSuccess: () => {
// Invalidate the todos cache after adding a new one
queryClient.invalidateQueries('todos');
},
});
const handleAddTodo = () => {
mutation.mutate({ title: 'Learn React Query' });
};
return (
<button onClick={handleAddTodo}>
{mutation.isLoading ? 'Adding...' : 'Add Todo'}
</button>
);
}
Here’s what’s happening:
- We’re using
useMutation
to handle the server call that adds a new todo. - After the mutation succeeds, we invalidate the
todos
cache usingqueryClient.invalidateQueries('todos')
. - This triggers a re-fetch of the todos, ensuring that your UI reflects the most up-to-date data.
Cache invalidation is crucial in ensuring that your app doesn't serve outdated data to your users, keeping the UI always in sync with the backend.
Optimistic Updates: Because Waiting Is Overrated
Nobody likes waiting. Optimistic updates let you instantly update the UI before the server responds, giving users a smoother experience. React Query makes this super easy to implement.
const mutation = useMutation(addTodo, {
onMutate: async (newTodo) => {
// Cancel any outgoing queries for 'todos'
await queryClient.cancelQueries('todos');
// Snapshot the previous todos
const previousTodos = queryClient.getQueryData('todos');
// Optimistically update to the new value
queryClient.setQueryData('todos', (old) => [...old, newTodo]);
// Return context so we can rollback if the mutation fails
return { previousTodos };
},
onError: (err, newTodo, context) => {
// Rollback the cache if the mutation fails
queryClient.setQueryData('todos', context.previousTodos);
},
onSettled: () => {
// Always refetch after mutation
queryClient.invalidateQueries('todos');
},
});
Now your UI will update instantly when a new todo is added, and the mutation’s outcome will either commit the change or roll it back. Users will thank you for the snappy experience!
Wrapping Up
React Query isn’t just another data-fetching library—it’s a productivity booster, a headache eliminator, and a performance optimizer all in one. By handling server-state with features like caching, cache invalidation, and optimistic updates, React Query takes the burden of managing data out of your hands.
So, if you’re tired of fighting with useEffect
, give React Query a try. It might just feel like magic.
Now go forth and fetch data, invalidate caches, and never worry about stale state again! Your future self will thank you.
Peace out! 👋🏻
Posted on September 9, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.