Why use React Query?
React Query simplifies data fetching, synchronization, and updating async states in React.
Provides hooks and utilities to manage complicated stuff related to API calls and updates in a react application, simplifies stuff like caching and automatic refetching :]
Super simple example of fetching plans
from the backend using useQuery
hook
interface Params { id: string } export const getPlans = async ({ id }: Params) => { if (!id) return []; const endpoint = `https://example-endpoint.app/${resumeId}`; const headers = new Headers(); const authToken = await auth.currentUser?.getIdTokenResult(true); headers.append('Authorization', `Bearer ${authToken?.token}`); const res = await window.fetch(endpoint, { method: 'GET', headers: headers, }); const data = await res.json(); return data; }; export const useGetPlans = (params: Params) => { return useQuery({ queryKey: ['plans', params], // Dependency array, when params change, this query is automatically fetched queryFn: () => getPlans(params), }); };
How it will be used in the frontend:
export default function DisplayPlans() { const { data: plans, isLoading } = useGetPlans({ id: selectedSomething?.id, }); return ( // display plans here <> {isLoading ? ( <LoadingComponent /> ) : ( <div className="flex w-full flex-col items-start gap-2 self-stretch transition-height duration-500"> {plans?.map((plan: IPlan) => { // Display the plans } })} </div> )} </> ) }
Retrieving (fetching) data: useQuery
Once you define the dependencies, React Query takes care of fetching the data (along with performing smart updates whenever necessary), making sure that the data in the backend is the same as (in sync with) data in the front-end
Updating data: useMutation
Mutations are for updating data in the backend.
Changes made by the mutation aren’t coupled to the queries, so that has to be done manually by invalidating the queries which will automatically fetch the new data from the backend.
Code example from tkdodo.eu
import { useMutation, useQueryClient } from 'react-query'; import axios from 'axios'; const useAddComment = (id: number) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: (newComment: any) => axios.post(`/posts/${id}/comments`, newComment), onSuccess: () => { // ✅ refetch the comments list for our blog post queryClient.invalidateQueries({ queryKey: ['posts', id, 'comments'] }); }, }); }; export default useAddComment;
Its also possible to do direct updates to the query cache like:
onSuccess: (newPost) => { queryClient.setQueryData(['posts', id], newPost) }
onSuccess: (newPost) => {
11 // ✅ update detail view directly
12 queryClient.setQueryData(['posts', id], newPost)
13 },
BUT THIS IS NOT RECOMMENDED
Sorted lists are for example pretty hard to update directly, as the position of entries could've potentially changed because of the update. Invalidating the whole list is the "safer" approach.
More about useMutation
- Using callbacks properly to avoid callbacks not firing
You can have callbacks on
useMutation
as well as on mutate
itself but callbacks on useMutation
fires before the callbacks on mutate
.Example:
const useUpdateTodo = () => useMutation({ mutationFn: updateTodo, // This is the callback on useMutation onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos', 'list'] }) }, }) // in the component const updateTodo = useUpdateTodo() updateTodo.mutate( { title: 'newTitle' }, // when the mutation finishes, // THIS MIGHT NOT GET FIRED IF COMPONENT IS UNMOUNTED BEFORE MUTATION FINISHES { onSuccess: () => history.push('/todos') } )
Good practices (recommendation by tkdodo.eu)
- Do things that are absolutely necessary and logic related (like query invalidation) in the
useMutation
callbacks.
- Do UI related things like redirects or showing toast notifications in
mutate
callbacks. If the user navigated away from the current screen before the mutation finished, those will purposefully not fire.