最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to add debounce to react onChange Input - Stack Overflow

programmeradmin3浏览0评论

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(`/${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
            }
...

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/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
            }
...
Share Improve this question edited Nov 28, 2020 at 16:34 marc_s 755k184 gold badges1.4k silver badges1.5k bronze badges asked Oct 28, 2020 at 18:27 sevenseven 1851 gold badge4 silver badges17 bronze badges 5
  • 1 Does this answer your question? How to debounce a callback in functional ponent using hooks – armin yahya Commented Oct 28, 2020 at 19:09
  • uniformly no, already tried that – seven Commented Oct 28, 2020 at 20:55
  • This is a controlled input ponent, so if you really want to debounce the handler directly it won't update the box with typed characters, and further, subsequent change events will only give you the newest character to the last debounced input. See fiddle. For the box to even function you'll need to track diffs of the values over the debounce window. Is this really what you want? – concat Commented Oct 29, 2020 at 7:21
  • no, not at all. I'm looking to dispatch action after x seconds – seven Commented Oct 29, 2020 at 15:26
  • Use redux-saga. It is used for side-effects and by default it supports debouncing, throttling, etc. – Deadpool Commented Nov 20, 2020 at 9:50
Add a ment  | 

3 Answers 3

Reset to default 4

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


Or just use https://www.npmjs./package/use-debounce

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 ponents 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/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)} />;
};

I've seen React Redux projects use this resource for adding debounce to their project. It's taken from https://davidwalsh.name/javascript-debounce-function

export const debounce = (func, wait, immediate) => {
    let timeout;
    return function () {
        const later = () => {
            timeout = null;
            if (!immediate) func.apply(this, arguments);
        };
        const callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(this, arguments);
    };
};
发布评论

评论列表(0)

  1. 暂无评论