React: How to prevent unnecessary api calls
Using lodash-debounce or axios-cancellation in functional component
Have you an experience like this when you send an api call with input element? Every time user types something, it sends an api call to server. It is good practice if we can prevent to send unnecessary api calls. It will reduce http requests to server and optimize components because of reduced unnecessary re-rendering.
Solution
We can achieve this easily with lodash-debounce
or axios cancellation
!
Lodash-debounce
Lodash is a JavaScript library which provides utility functions for common programming tasks using the functional programming paradigm — Wikipedia
The Debounce function groups multiple sequential calls in a single one. Callback function passed though debounce fires after delayed time. So it would be a perfect solution for sending api calls when query is often changed in short time, typically with onChange
event.
_.debounce(callback, delay)
Debounce function works with setTimeout
behind the scenes. So when component is re-rendered, setTimeout callback
is also re-created. So to use debounce
in functional component, we have two options.
- useRef
- useCallback
useRef
import { debounce } from 'lodash';const Search = () => {
const [userQuery, setUserQuery] = useState("");
const delayedQuery = useRef(debounce(q => sendQuery(q), 500)).current;
const onChange = e => {
setUserQuery(e.target.value);
delayedQuery(e.target.value);
}; return <input onChange={onChange} value={userQuery} />
};
useCallback
const delayedQuery = useCallback(debounce(q => sendQuery(q), 500), []);
In my opinion, useCallback
is probably better than useRef
. It has shorter syntax.
Example Code
import { useState, useEffect, useCallback } from 'react';
import axios from 'axios';
import { MovieList } from 'types/types';
import { debounce } from 'lodash';
function useFetch(query: string, pageNum: number) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(false);
const [list, setList] = useState<MovieList>([]);
const [hasMore, setHasMore] = useState(false);
const delayedQuery = useCallback(
debounce((q: string) => sendQuery(q), 500), // (*)
[]
);
const sendQuery = useCallback(
async (query: string): Promise<any> => {
if (query === '') return;
try {
await setIsLoading(true);
let res = await axios.get(url);
await setList(prev => [...prev, ...res.data]);
await setHasMore(data?.length > 0);
setIsLoading(false);
} catch (error) {
setError(error);
}
},
[pageNum]
);
useEffect(() => {
setList([]);
}, [query]);
useEffect(() => {
delayedQuery(query); // (*)
}, [query, pageNum, delayedQuery]);
return { isLoading, error, list, hasMore };
}
export default useFetch;
Axios Cancellation
You can cancel a request using a cancel token.The axios cancel token API is based on the withdrawn cancelable promises proposal. Cancellation support was added in axios v0.15.— axios docs
To cancel previous request, you need to create cancelTocken
. cancelTocken
can be created in two ways.
cancelTocken.source
- Passing an executor function to the
CancelToken
constructor
cancelTocken.source
const cancelTokenSource = axios.CancelToken.source();
axios.get(url, {
cancelToken: cancelTokenSource.token
});
cancelTokenSource.cancel(); // Cancel request
Passing an executor function to the CancelToken
constructor
const CancelToken = axios.CancelToken;
let cancel;
axios.get(url, {
cancelToken: new CancelToken((c) => cancel = c })
});
cancel(); // cancel the request
Example Code 1 (useEffect)
Let’s cancel previous request with cancelTocken
in functional component.
useEffect(() => {
const request = Axios.CancelToken.source() // (*)
const fetchPost = async () => {
try {
const response = await Axios.get(`endpointURL`, {
cancelToken: request.token, // (*)
})
setPost(response.data)
setIsLoading(false)
} catch (err) {
console.log('There was a problem or request was cancelled.')
}
}
fetchPost()
return () => request.cancel() // (*)
}, [])
Example Code 2 (useFetch custom hook)
function useFetch(query: string, pageNum: number) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(false);
const [list, setList] = useState<MovieList>([]);
const [hasMore, setHasMore] = useState(false);
const sendQuery = useCallback(
async (query: string): Promise<any> => {
const CancelToken = axios.CancelToken; // (*)
let cancel: () => void; // (*)
if (query === '') return;
try {
await setIsLoading(true);
let res = await axios.get(url, {
cancelToken: new CancelToken(c => cancel = c), // (*)
});
await setList(prev => [...prev, ...res.data]);
await setHasMore(data?.length > 0);
setIsLoading(false);
} catch (error) {
if (axios.isCancel(error)) return; // (*)
setError(error);
}
return () => cancel(); // (*)
},
[pageNum]
);
useEffect(() => {
setList([]);
}, [query]); useEffect(() => {
sendQuery(query);
}, [query, pageNum, sendQuery]); return { isLoading, error, list, hasMore };
}export default useFetch;
Conclusion
Reducing unnecessary api calls is pretty important for optimizing of your application. You can consider using lodash-debounce or axios cancellation. They are really easy to use, so I highly recommend to try them.