Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add debounce to react onChange Input

I'm trying to add debounce functionality to my React app, and want to do it without a library like loadash, 3rd party node module, etc. I tried with some posts there, but nothing worked for me.

Basically, in handleSearch I just dispatch redux action which performs a query to API endpoint and in input bind value to pice of redux state and call handleSearch onChange.

My code is:

const handleSearch = (e: React.FormEvent<HTMLInputElement>) => {
        const value = e.currentTarget.value
        dispatch(setSearch(value))
    }

And later in return

<input type="text" value={searchQuery} onChange={handleSearch} />

Also, my action:

export const searchMovies = (category: String, searchQuery: string) => async (dispatch: Dispatch<ControlDispatchTypes>) => {
    try {
        dispatch({
            type: SEARCH_LIST_LOADING
        })
        let res: any;

        if (searchQuery.length >= 3) {
            res = await axios.get(`https://api.themoviedb.org/3/search/${category}?api_key=xxxxxxxxxx&query=${searchQuery}`)
        }

        dispatch({
            type: SEARCH_LIST_SUCCESS,
            payload: res.data.results
        })

    } catch (error) {
        dispatch({
            type: SEARCH_LIST_FAIL
        })
    }
}

and a piece of the reducer used for the search:

...
 case SET_SEARCH:
            return {
                ...state,
                search: action.payload
            }
        case SEARCH_LIST_LOADING:
            return {
                ...state,
                searchLoading: false
            }
        case SEARCH_LIST_SUCCESS:
            return {
                ...state,
                searchLoading: false,
                items: action.payload

            }
        case SEARCH_LIST_FAIL:
            return {
                ...state,
                searchLoading: false
            }
...
like image 518
seven Avatar asked Oct 28 '20 18:10

seven


People also ask

How do you use debounce in onChange React?

Here's a simple implementation : import React, { useCallback } from "react"; import { debounce } from "lodash"; const handler = useCallback(debounce(someFunction, 2000), []); const onChange = (event) => { // perform any event related action here handler(); };

How do you make a debounce in React JS?

Another way to implement debouncing is using lodash. Lodash provides a debounce method that we can use to limit the rate of execution of the handleChange function. Just wrap the callback function with the debounce method and provide the amount of delay we want between two events.


2 Answers

It isn't clear how you are calling searchMovies() but I think the easiest way of doing this kind of thing is by wrapping your dispatch in a callback that's been debounced.

const debounce = (fn, delay) => {
  let timeout = -1;

  return (...args) => {
    if (timeout !== -1) {
      clearTimeout(timeout);
    }

    timeout = setTimeout(fn, delay, ...args);
  };
};

export const App = () => {
  const query = useSelector(selectQuery);
  const dispatch = useDispatch();
  
  const requestMovies = useMemo(() => {
    return debounce((query) => {
      dispatch(searchMovies(query))
    }, 300);
  }, []);

  const onQueryChange = useCallback((q) => {
    dispatch(setSearch(q));
    requestMovies(q);
  }, []);

  return (
    <div className="App">
      <input value={query} onChange={(e) => onQueryChange(e.currentTarget.value)} />
    </div>
  );
}

That is the simple solution. If you want bonus points consider writing a custom hook to pull contain all of that state and logic. It cleans up the components and makes the movie search reusable.

const QUERY_DEBOUNCE_PERIOD = 400;

const useMovieSearch = () => {
  const dispatch = useDispatch();
  const query = useSelector(selectQuery);
  const category= useSelector(selectCategory);
  const fence = useRef("");

  const requestMovies = useMemo(() => {
    const request = async (category, query) => {
      const uri = `https://api.themoviedb.org/3/search/${category}?api_key=xxxxxxxxxx&query=${query}`;
      fence.current = uri;

      dispatch({
        type: SEARCH_LIST_LOADING
      });

      try {
        if (query.length >= 3) {
          const res = await axios.get(uri);

          if (fence.current === uri) {
            dispatch({
              type: SEARCH_LIST_SUCCESS,
              payload: res.data.results
            });
          }
        }
      } catch (error) {
        dispatch({
          type: SEARCH_LIST_FAIL
        })
      }
    };

    return debounce(request, QUERY_DEBOUNCE_PERIOD);
  }, []);

  const searchMovies = useCallback((category, query) => {
    dispatch({ type: SET_SEARCH, payload: query });
    requestMovies(category, query);
  }, [requestMovies]);

  return {
    query,
    category,
    searchMovies
  };
};

This should look like pretty standard stuff. I just moved all of the searchMovies logic into a custom hook. I also added fencing. Because of the asynchrony of the internet, you are not guaranteed to get results in the order the request were sent out. This simply ignores all responses except for the most recent request.

Usage is pretty much what you'd expect.

const SearchBar = () => {
  const [query, category, searchMovies] = useMovieSearch();

  return <input value={query} onChange={(e) => searchMovies(category, e.currentTarget.value)} />;
};
like image 55
Brenden Avatar answered Oct 20 '22 14:10

Brenden


I created a custom hook that encapsulates the debouncing.

import { useMemo, useState } from 'react';

const debounce = (fn: any, delay: number) => {
  let timeout = -1;
  return (...args: any[]) => {
    if (timeout !== -1) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(fn, delay, ...args);
  };
};

export function useStateDebounced<T>(initialValue: T, delay: number) {
  const [inputValue, _setInputValue] = useState<T>(initialValue);

  const [debouncedInputValue, setDebouncedInputValue] = useState<T>(
    initialValue
  );

  const memoizedDebounce = useMemo(
    () =>
      debounce((value: T) => {
        setDebouncedInputValue(value);
      }, delay),
    [delay]
  );

  const setInputValue = (value: T | ((prevState: T) => T)) => {
    if (value instanceof Function) {
      _setInputValue((p) => {
        const mutated = value(p);
        memoizedDebounce(mutated);
        return mutated;
      });
    } else {
      _setInputValue(value);
      memoizedDebounce(value);
    }
  };

  return [inputValue, debouncedInputValue, setInputValue] as const;
}

Usage in code:

export default function App() {
  const [value, debouncedValue, setValue] = useStateDebounced('', 1000);

  const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };

  return (
    <div className="App">
      {debouncedValue}
      <input type="text" value={value} onChange={handleInputChange} />
    </div>
  );
}

View on CodeSandbox


Edit:

Just use https://www.npmjs.com/package/use-debounce

like image 1
cgatian Avatar answered Oct 20 '22 14:10

cgatian