53
loading...
This website collects cookies to deliver better user experience
debounce
and throttle
and their real-life use cases (skip ahead if you are already familiar with these concepts).debounce
interval of 500ms means that if 500ms hasn’t passed from the previous invocation attempt, we cancel the previous invocation and schedule the next invocation of the function after 500ms.debounce
is a Typeahead
.throttle
interval of 500ms, if we try to invoke a function n times within 500ms, the function is called only once when 500ms has elapsed from the beginning.Throttle
is commonly used with resize
or scroll
events.throttle
/debounce
in functional components with Hooks, let’s quickly see how we do it in a class component.Note: I am using lodash debounce
and throttle
functions in this article.
import React from "react";
export default class App extends React.Component {
constructor(props) {
super(props);
this.debouncedOnChange = _.debounce(this.handleChange, 300);
this.debouncedHandleWindowResize = _.throttle(this.handleWindowResize, 200);
}
handleChange = (_, property) => {
// your logic here
};
handleWindowResize = (_, property) => {
// your resize logic here
};
// rest of rendering code
}
import React from "react";
import _ from "lodash";
export default function App() {
const onChange = () => {
// code logic here
};
const handleWindowResize = () => {
// code logic here
};
const debouncedOnChange = _.debounce(onChange, 300);
const throttledHandleWindowResize = _.throttle(handleWindowResize, 300);
//rendering code here
}
debounce
/throttle
in functional components.useCallback
Hook.useCallback
:“Pass an inline callback and an array of dependencies. useCallback
will return a memoized version of the callback that only changes if one of the dependencies has changed.”
import React, { useState, useEffect, useCallback } from "react";
import _ from "lodash";
export default function App() {
const [inputValue, setInputValue] = useState("");
const onChange = () => {
console.log('inputValue', inputValue);
// other logic here
};
//debounced onChange functin
const debouncedOnChange = useCallback(_.debounce(onChange, 300), [inputValue]);
const handleWindowResize = useCallback((_, property) => {
// logic here
}, []);
const throttledHandleWindowResize = useCallback(
_.throttle(handleWindowResize, 300),
[]
);
const handleChange = e => {
setInputValue(e.target.value);
};
useEffect(() => {
onChange();
debouncedOnChange();
}, [inputValue]);
// other code here
}
onChange
handler makes use of the enclosing state inputValue
. So when we create the memoized debounced function with useCallback
, we pass inputValue
in the dependency array of useCallback
. Otherwise, the values obtained in the function call will be stale values instead of the updated ones due to closures.inputValue
changes. However, the input value changes every time we want to call the function, so we will still face the same problem of a new reference getting created. The net result is that our function still doesn’t work as expected.useCallback
can help if we are able to create the instance of the debounced or throttled function only on the initial render, so can we solve the problem of stale closures without having to add a dependency to useCallback
?Keeping a copy of our state in ref
: Since refs
are mutated, they aren’t truly affected by closures in the sense that we can still see the updated value even if the reference is old. So whenever we are updating the state, we also update the ref
. We shall not go down this path unless it’s a last resort, as it is a bit hacky and involves a lot of state duplication, which isn’t ideal.
Pass values as arguments: Instead of relying on closures to use a value, we can pass all the necessary values that our function needs as arguments.
import React, { useState, useEffect, useCallback } from "react";
import _ from "lodash";
export default function App() {
const [inputValue, setInputValue] = useState("");
const [debounceValues, setDebounceValues] = useState({
nonDebouncedFuncCalls: 0,
debouncedFuncCalls: 0
});
const [throttleValues, setThrottleValues] = useState({
nonThrottledFunctionCalls: 0,
throttledFuntionCalls: 0
});
const onChange = (property, inputValue) => {
console.log(`inputValue in ${property}`, inputValue);
setDebounceValues(prev => ({
...prev,
[property]: prev[property] + 1
}));
};
const handleWindowResize = useCallback((_, property) => {
setThrottleValues(prev => ({
...prev,
[property]: prev[property] + 1
}));
}, []);
const debouncedOnChange = useCallback(_.debounce(onChange, 300), []);
const throttledHandleWindowResize = useCallback(
_.throttle(handleWindowResize, 300),
[]
);
const handleChange = e => {
const value = e.target.value;
setInputValue(value);
onChange("nonDebouncedFuncCalls", value);
debouncedOnChange("debouncedFuncCalls", value);
};
const onWindowResize = useCallback(e => {
handleWindowResize(e, "nonThrottledFunctionCalls");
throttledHandleWindowResize(e, "throttledFuntionCalls");
}, []);
useEffect(() => {
window.addEventListener("resize", onWindowResize);
return () => {
window.removeEventListener("resize", onWindowResize);
};
}, [onWindowResize]);
//rest of the rendering code
}
inputValue
as an argument to the debounced function and thus ensuring that it has all the latest values it needs and works smoothly.Note: Using functional state updates can also help to avoid passing every property — especially if it just has to update the state.
useCallback
, we can also use useMemo
, but the main approach logic will remain the same.debounce
with React Hooks. These are the key takeaways:We need to use the same instance of the created function as much as possible.
Use the useCallback/useMemo
Hook to memoize our created functions.
To avoid closure issues and also prevent the function from getting recreated, we can pass the values needed by the function as arguments.
State updates that need previous values can be implemented using the functional form of setState
.