This hook is for when we want to track something that changes quickly (mouse moves, user typing, etc...), but wait to trigger the onChange
until the updates stop streaming in. You'd typically do this when your onChange
does something expensive, like a make network call.
In the following example, pretend onChange
prop is a function that makes an API call to the server. Here's where we'll get by the end of the Post.
function SearchBox({ onChange, defaultValue = "" }) {
// We store one value for the instant updates
let [value, setValue] = useState(defaultValue);
useDebouncedEffect(
(debouncedValue) => {
console.log("Firing onchange", debouncedValue);
onChange(debouncedValue);
},
value,
300
);
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
A simple first approach might be to leverage useEffect
, but we'd be making a network call for every keystroke.
function SearchBox({ onChange, defaultValue = "" }) {
// We store one value for the instant updates
let [value, setValue] = useState(defaultValue);
// Whenever the value changes, call the passed in 'onChange'
useEffect(() => {
console.log("Firing onchange", value);
onChange(value);
}, [value]);
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
We want to create a way to only trigger the onChange
once the value has stopped updating for a specified amount of time.
Lets create a custom hook, useDebouncedValue
, that keeps track of a frequently changing value with state, but only updates the state once the value stops updating.
export function useDebouncedValue(value, delay) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// Update state to the passed in value after the specified delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
// If our value changes (or the component unmounts), React will
// run this cleanup function to cancel the state update.
clearTimeout(handler);
};
// These are the dependencies, if the value or the delay amount
// changes, then cancel any existing timeout and start waiting again
}, [value, delay]);
return debouncedValue;
}
Now we could use our new hook like so:
function SearchBox({ onChange, defaultValue = ""}) {
// We store one value for the instant updates
let [value, setValue] = useState(defaultValue);
// We use our new hook to track a value that only changes
// when the user stops typing
let debouncedValue = useDebouncedValue(value, 300);
// We perform an effect anytime the user stops typing
useEffect(() => {
console.log("Firing onchange", debouncedValue);
onChange(debouncedValue);
}, [debouncedValue]);
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
That works. But what if we tried to get rid of some of the boilerplate with one more custom hook, useDebouncedEffect
.
export function useDebouncedEffect(effectFn, value, delay = 250) {
// Store the effect function as a ref so that we don't
// trigger a re-render each time the function changes
let effectRef = useRef(effectFn);
// Leverage the hook we just created above
let debouncedValue = useDebouncedValue(value, delay);
// Run an effect whenever the debounced value
useEffect(() => {
if (effectRef.current) {
// Invoke the effect function, passing the debouncedValue
return effectRef.current(debouncedValue);
}
}, [debouncedValue]);
}
The final solution feels very similar to the original useEffect
strategy but we get the performance benefits of the debounce.
function SearchBox({ onChange, defaultValue = "" }) {
// We store one value for the instant updates
let [value, setValue] = useState(defaultValue);
useDebouncedEffect(
(debouncedValue) => {
console.log("Firing onchange", debouncedValue);
onChange(debouncedValue);
},
value,
300
);
return <input value={value} onChange={(e) => setValue(e.target.value)} />;
}
Top comments (0)