Simple Server State management with React Query
React Query is a library that provides hooks for fetching, caching, and updating data in React applications without touching any global state. It helps manage server state which is asynchronous and often not included in the typical state management libraries like Redux or Context API.
Setting Up React Query
To get started, you need to install React Query:
npm install react-query
Once installed, you'll wrap your application with the QueryClientProvider and create a QueryClient instance:
import { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* The rest of your application */}
</QueryClientProvider>
);
}
export default App;
Fetching Data with useQuery
Let's fetch some data using the useQuery
hook. We will use a simple example where we fetch a list of todos from a JSON placeholder API:
import { useQuery } from 'react-query';
const fetchTodos = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
};
function Todos() {
const { data, error, isLoading } = useQuery('todos', fetchTodos);
if (isLoading) return 'Loading...';
if (error) return 'An error has occurred: ' + error.message;
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
In this example, useQuery
is called with two arguments: a unique key for the query ('todos') and a function (fetchTodos)
that returns a promise which resolves to the data.
Mutations with useMutation
For creating or updating data, React Query provides the useMutation
hook. Here's how you might use it to add a new todo:
import { useMutation, useQueryClient } from 'react-query';
const addTodo = async (newTodo) => {
const response = await fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newTodo),
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
};
function AddTodo() {
const queryClient = useQueryClient();
const mutation = useMutation(addTodo, {
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries('todos');
},
});
const handleSubmit = (event) => {
event.preventDefault();
const newTodo = { title: 'Do laundry', completed: false };
mutation.mutate(newTodo);
};
return (
<form onSubmit={handleSubmit}>
<button type="submit">Add Todo</button>
</form>
);
}
When the addTodo
mutation is successful, we call queryClient.invalidateQueries()
to refetch the todos
and update the UI with the new todo.
Optimistic Updates
React Query shines with optimistic updates, which allow the UI to update immediately before the mutation is confirmed by the server:
function ToggleTodo({ todo }) {
const queryClient = useQueryClient();
const mutation = useMutation(
newTodo => fetch(`https://jsonplaceholder.typicode.com/todos/${newTodo.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newTodo),
}),
{
// When mutate is called:
onMutate: async newTodo => {
await queryClient.cancelQueries('todos');
const previousTodos = queryClient.getQueryData('todos');
queryClient.setQueryData('todos', old =>
old.map(item => item.id === newTodo.id ? { ...item, ...newTodo } : item)
);
return { previousTodos };
},
// If the mutation fails, roll back to the previous value
onError: (err, newTodo, context) => {
queryClient.setQueryData('todos', context.previousTodos);
},
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries('todos');
},
}
);
const toggle = () => {
mutation.mutate({ ...todo, completed: !todo.completed });
};
return (
<li onClick={toggle} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.title}
</li>
);
}
In this code, onMutate
performs the optimistic update by immediately updating the todos query data. If the mutation fails, onError
rolls back to the previous state.
Conclusion
React Query offers a robust set of tools for managing server state in React applications. By leveraging its capabilities for data fetching, mutations, and optimistic updates, developers can greatly simplify state management and improve the user experience with less loading time and more immediate responses to user actions. As a result, React Query has become an essential part of the modern React developer's toolkit.