React: How to prevent unnecessary api calls

Using lodash-debounce or axios-cancellation in functional component

Suyeon Kang
suyeonme
4 min readFeb 11, 2021

--

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.

Example

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

https://css-tricks.com/debouncing-throttling-explained-examples

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.

  1. useRef
  2. 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.

  1. cancelTocken.source
  2. 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.

--

--