React Query is using to create a request state short-hand in your react apps. In this blog we gonna show how to use it in most use case and explain what happen when use react query.
This blog base on React Query v4
Fetch with useQuery
simple request with useQuery. It will return data, loading status or error
export const useProfile = () => {
return useQuery(['profile'], () => {
return axios.get('/profile')
})
};
// React Component
const { status, error, data } = useProfile()
Notes
- useQuery will retry when get and error response (status code = 5xx) from API
Status Handling
you can use status
enum or boolean flag
using isLoading, isFetching, isError, isSuccess to handle a status of data request.
// Status Flag
const { status, fetchStatus, error, data } = useProfile()
status = 'loading' | 'fetching' | 'error' | 'success'
// Boolean Flag
const { isLoading, isFetching, isError, isSuccess, error, data } = useProfile()
Difference between isLoading & isFetching
- Loading
- Loading handle normal state of API request
- Loading is trigger only first fetching. Background fetching or refetch() function will not trigger loading flag.
- Fetching
- Fetching handle a state of network status. Every API request include background request will trigger fetching state.
- Background fetching data and refetch() manually will trigger isFetching flag.
Global Fetching Flag
If your need to display loading on every request in your app. Such as global loading status.
import { useIsFetching } from '@tanstack/react-query'
const isFetching = useIsFetching()
Re-fetching Data Strategy
1. Window Focus
When your go to other tab on come back to your web application again. react-query will automatically reload data from api to get a fresh data. If it not useful for you. You can disable it.
*Note: Window Focus refetch is in background mode. loading
= false / fetching
= true
// Disable with useQuery with referchOnWindowFocus flag.
export const useFindProject = (id: string) => {
return useQuery(["project", id], () => {
return axios.get(`/products/${id}`);
}, {
refetchOnWindowFocus: false
});
};
// Disable Globally via QueryClient Options
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
})
2. Interval
You can tell react-query to make a re-fetching loop on every x seconds using interval. Useful for auto updating data or realtime data without using websocket.
*Note: Interval refetch is in background mode. loading
= false / fetching
= true. Only first request that loading flag = true.
export const useFindProject = (id: string) => {
return useQuery(["project", id], () => {
return axios.get(`/products/${id}`);
}, {
refetchInterval: 5000 // In milliseconds
});
};
3. Manual
using refetch
function to manual re-fetching data from API.
*Note: Window Focus refetch is in background mode. loading
= false / fetching
= true
export const useProfile = () => {
return useQuery(['profile'], () => {
return axios.get('/profile')
})
};
const { status, error, data, refetch } = useProfile()
// Call refetch function to manually reload data from api
refetch()
4. Invalidate Query to clear out-of-date data.
Another way to make react query handle re-fetching data is using Invalidate Query.
const queryClient = useQueryClient()
queryClient.invalidateQueries({ queryKey: ['todos', 'tags'] })
// this useQuery will be clear data and fetch new one from api
useQuery(["todos"], () => {
return axios.get(`/todos`);
});
useQuery(["tags"], () => {
return axios.get(`/tags`);
});
A Pros of using invalidate query method is you can re-fetching another data using key of useQuery without import that function and use a refetch method. one or more useQuery key at once. Best suite for many situation. For example, you add new todo with tag and need to refetching a todos list and tags list from API.
To clear all of useQuery cache in your app at once.
const queryClient = useQueryClient()
// Clear all query cache and every useQuery will refetch new data
queryClient.invalidateQueries()
Retry in useQuery
By default, react-query will retry useQuery 4xx, 5xx error response. You can disable it with retry flag or limit number of retry
export const useResetPasswordSession = (token: string) => {
return useQuery(["reset-password", token], () => {
return axios.get(`/reset-password/${token}`);
}, {
enabled: token ? true : false,
retry: false
});
};
Custom Retry Function
Some of API use error 4xx to handle a response of data. For example, an API /products/12
that will find a product id = 12 but if it not exist, API return 404 Not Found. In this case, I think it should not to retry and error will raise to tell user that data not found ( 404 Not Found Page ).
export const useResetPasswordSession = (token: string) => {
return useQuery(["reset-password", token], () => {
return axios.get(`/reset-password/${token}`);
}, {
enabled: token ? true : false,
retry: (failedCount: number, err: AxiosError) => {
// No retry when api response is 404 Not Found
if(err.response.status == 404) {
return false;
}
return true;
}
});
};
Error Handler
when useQuery get error response with http status code 4xx, 5xx. isError
flag will be true. But it will retry until reach retry limit. error
will be error response from API.
For example using request with axios
, error will be a AxiosError instance
const { isError, error } = useProfile()
if(isError) {
console.log(error)
// AxiosError
// {
// statusCode: 404,
// data: {}
// }
}
Handle error in useQuery should be careful with retry. when react query retry and make request to API. isError will be set to false.
useQuery with parameters
export const useProject = (id: string) => {
return useQuery(['project', id], () => {
return axios.get(`/products/${id}`)
})
};
const { status, error, data } = useProject(1)
export const useProducts = (queryParams: object) => {
return useQuery(['project', queryParams], () => {
return axios.get(`/products`, {
params: queryParams
})
})
};
const { status, error, data } = useProducts({ isActive: true })
// if query params changed useProducts will be reload data again
Conditional in useQuery
If your need to call API after some flag such as user should be authenticated before fetching this API. use enabled
flag. useQuery will call and api when enabled flag is true.
export const useProducts = (queryParams: object) => {
const { isAuthenticated } = useAuth();
return useQuery(["project", queryParams], () => {
return axios.get(`/products`, {
params: queryParams,
});
}, {
enabled: isAuthenticated
});
};
POST using useMutation
When you need to send data to API via POST / PUT / DELETE method.
export const useUpdateProfile = () => {
return useMutation((data: User) => {
return axios.post('/profile', data)
})
};
// Using
const updater = useUpdateProfile()
const onSubmit = async (dto: User) => {
try {
const result = await updater.mutateAsync(dto)
// Success
} catch(error) {
// Error Handling
}
}
// Status Flag
updater.status = 'loading' | 'error' | 'success'
CRUD with React Query
Here is full example when using react-query in full CRUD using useQuery to fetch data and useMutation to manipulate data.
// Fetch List without paginate
export const useGetProducts = (queryParams: object) => {
const { isAuthenticated } = useAuth();
return useQuery(["project", queryParams], () => {
return axios.get(`/products`, {
params: queryParams,
});
}, {
enabled: isAuthenticated
});
};
// Fetch One using ID
export const useFindProject = (id: string) => {
const { isAuthenticated } = useAuth();
return useQuery(["project", id], () => {
return axios.get(`/products/${id}`);
}, {
enabled: (id && isAuthenticated) ? true : false
});
};
// Create
export const useCreateProject = () => {
return useMutation((data: Project) => {
return axios.post("/products", data);
});
};
// Update
export const useUpdateProject = (id: string) => {
return useMutation((data: Project) => {
return axios.put(`/products/${id}`, data);
});
};
// Delete
export const useDeleteProject = (id: string) => {
return useMutation(() => {
return axios.delete(`/products/${id}`);
});
};