Get axios responses in the same order as requests for search functionality

Viewed 540

I'm currently working on a search functionality in React Native using axios.

When implementing search functionality i'm using debounce from lodash to limit the amount of requests sent.

However, since request responses are not received in same order there is a possibility of displaying incorrect search results.

For example when the user input 'Home deco' in input field there will be two requests.

One request with 'Home' and next with 'Home deco' as search query text.

If request with 'Home' takes more time to return than second request we will end up displaying results for 'Home' query text not 'Home deco'

Both results should be displayed to the user sequentially, if responses are returned in order but if 'Home' request is returned after 'Home deco' request then 'Home' response should be ignored.

Following is a example code

function Search (){
    const [results, setResults] = useState([]);
    const [searchText, setSearchText] = useState('');

    useEffect(() => {
            getSearchResultsDebounce(searchText);
    }, [searchText]);

    const getSearchResultsDebounce = useCallback(
        _.debounce(searchText => {
            getSearchResults(searchText)
        }, 1000),
        []
    );

    function getSearchResults(searchText) {

        const urlWithParams = getUrlWithParams(url, searchText);
        axios.get(urlWithParams, { headers: config.headers })
             .then(response => {
              if (response.status === 200 && response.data) 
              {
                setResults(response.data);

              } else{
                  //Handle error
              }
            })
            .catch(error => {
                //Handle error
            });
    }

    return (
     <View>
        <SearchComponent onTextChange={setSearchText}/>
        <SearchResults results={results}/>
     </View>
    )

}

What is the best approach to resolve above issue?

2 Answers

If you want to avoid using external libraries to reduce package size, like axios-hooks, I think you would be best off using the CancelToken feature included in axios.

Using the CancelToken feature properly will also prevent any warnings from react about failing to cancel async tasks.

Axios has an excellent page explaining how to use the CancelToken feature here. I would recommend reading if you would like a better understanding of how it works and why it is useful.

Here is how I would implement the CancelToken feature in the example you gave:

OP clarified in the replies that they do not want to implement a cancelation feature, in that case I would go with a timestamp system like the following:

function Search () {
    //change results to be a object with 2 properties, timestamp and value, timestamp being the time the request was issued, and value the most recent results
    const [results, setResults] = useState({
        timeStamp: 0,
        value: [],
    });
    const [searchText, setSearchText] = useState('');

    //create a ref which will be used to store the cancel token
    const cancelToken = useRef();
   
    //create a setSearchTextDebounced callback to debounce the search query
    const setSearchTextDebounced = useCallback(
        _.debounce((text) => {
            setSearchText(text)
        ), [setSearchText]
    );
   
    //put the request inside of a useEffect hook with searchText as a dep
    useEffect(() => {
        //generate a timestamp at the time the request will be made
        const requestTimeStamp = new Date().valueOf();

        //create a new cancel token for this request, and store it inside the cancelToken ref
        cancelToken.current = CancelToken.source();            
        
        //make the request
        const urlWithParams = getUrlWithParams(url, searchText);
        axios.get(urlWithParams, { 
            headers: config.headers,

            //provide the cancel token in the axios request config
            cancelToken: source.token 
        }).then(response => {
            if (response.status === 200 && response.data) {
                //when updating the results compare time stamps to check if this request's data is too old
                setResults(currentState => {
                    //check if the currentState's timeStamp is newer, if so then dont update the state
                    if (currentState.timeStamp > requestTimeStamp) return currentState;
                  
                    //if it is older then update the state
                    return {
                        timeStamp: requestTimeStamp,
                        value: request.data,
                    };
                });
            } else{
               //Handle error
            }
        }).catch(error => {
            //Handle error
        });
        
        //add a cleanup function which will cancel requests when the component unmounts
        return () => { 
            if (cancelToken.current) cancelToken.current.cancel("Component Unmounted!"); 
        };
    }, [searchText]);

    return (
        <View>
            {/* Use the setSearchTextDebounced function here instead of setSearchText. */}
            <SearchComponent onTextChange={setSearchTextDebounced}/>
            <SearchResults results={results.value}/>
        </View>
    );
}

As you can see, I also changed how the search itself gets debounced. I changed it where the searchText value itself is debounced and a useEffect hook with the search request is run when the searchText value changes. This way we can cancel previous request, run the new request, and cleanup on unmount in the same hook.

I modified my response to hopefully achieve what OP would like to happen while also including proper response cancelation on component unmount.

We can do something like this to achieve latest api response.

function search() {
    ...
    const [timeStamp, setTimeStamp] = "";
    ...


    function getSearchResults(searchText) {

        //local variable will always have the timestamp when it was called
    const reqTimeStamp = new Date().getTime();

        //timestamp will update everytime the new function call has been made for searching. so will always have latest timestampe of last api call
    setTimeStamp(reqTimeStamp)

    axios.get(...)
        .then(response => {

            // so will compare reqTimeStamp with timeStamp(which is of latest api call) if matched then we have got latest api call response 
            if(reqTimeStamp === timeStamp) {
                return result; // or do whatever you want with data
            } else {

            // timestamp did not match
            return ;
            }

        })
        
     }

}

Related