How to make a custom debounce hook works with a useCallback?

1.8k views Asked by At

I did search for those related issues and found some solutions, but most about the lodash debounce. In my case, I create useDebounce as a custom hook and return the value directly.

My current issue is useCallback works with an old debounced value.

Here are my code snips.

//To makes sure that the code is only triggered once per user input and send the request then.  

export const useDebounce = (value, delay) => {
    const [debouncedValue, setDebouncedValue] = useState(value);

    useEffect(() => {
        const timeout = setTimeout(() => setDebouncedValue(value), delay);
        return () => clearTimeout(timeout);
    }, [value, delay]);

    return debouncedValue;
};

useDebounce works as expected

export const ShopQuantityCounter = ({ id, qty }) => {
    const [value, setValue] = useState(qty);
    const debounceInput = useDebounce(value, 300);

    const dispatch = useDispatch();

    const handleOnInputChange = useCallback((e) => {
        setValue(e.target.value);  

        console.info('Inside OnChange: debounceInput', debounceInput);
        
        // dispatch(updateCartItem({ id: id, quantity: debounceInput }));
    },[debounceInput]);

    console.info('Outside OnChange: debounceInput', debounceInput);

    // To fixed issue that useState set method not reflecting change immediately
    useEffect(() => {
        setValue(qty);
    }, [qty]);


    return (
        <div className="core-cart__quantity">
 
            <input
                className="core-cart__quantity--total"
                type="number"
                step="1"
                min="1"
                title="Qty"
                value={value}
                pattern="^[0-9]*[1-9][0-9]*$"
                onChange={handleOnInputChange}
            />
      
        </div>
    );
};

export default ShopQuantityCounter;

Here are screenshots with console.info to explain what the issue is.

Current quantity
Updated with onChange

I do appreciate it if you have any solution to fix it, and also welcome to put forward any code that needs updates.

1

There are 1 answers

3
Yadab On

This might help you achieve what you want. You can create a reusable debounce function with the callback like below.

export const useDebounce = (value, delay) => {
    const [debouncedValue, setDebouncedValue] = useState(value);
    let timeout;

    const setDebounce = (newValue) => {
        clearTimeout(timeout);
        timeout = setTimeout(() => setDebouncedValue(newValue), delay);
    };

    return [debouncedValue, setDebounce];
};

And use the function on your code like this.

export const ShopQuantityCounter = ({ id, qty }) => {
    const [value, setValue] = useState(qty);
    const [debounceInput, setDebounceInput] = useDebounce(value, 300);

    const dispatch = useDispatch();

    const handleOnInputChange = useCallback((e) => {
        setDebounceInput(e.target.value);
        console.info('Inside OnChange: debounceInput', debounceInput);
        
        // dispatch(updateCartItem({ id: id, quantity: debounceInput }));
    },[debounceInput]);

    console.info('Outside OnChange: debounceInput', debounceInput);

    // To fixed issue that useState set method not reflecting change immediately
    useEffect(() => {
        setValue(qty);
    }, [qty]);


    return (
        <div className="core-cart__quantity">
 
            <input
                className="core-cart__quantity--total"
                type="number"
                step="1"
                min="1"
                title="Qty"
                value={value}
                pattern="^[0-9]*[1-9][0-9]*$"
                onChange={handleOnInputChange}
            />
      
        </div>
    );
};

export default ShopQuantityCounter;