Search code examples
javascriptreactjsuse-effectusecallback

React useCallback with debounce works with old value, how to get actual state value?


I can not fulfill all the conditions:

  1. I need some function inside useCallback, because I set it as props to child component (for re-render preventing)
  2. I need to use debounce, because my function is "end point" and can be called ~100times/sec
  3. I need to get current (actual values) after debounce.

I have problem with last point, my values after debounce (1000ms) is outdated.

How to get current values using useCallback + debounce ? (values in alert must to be same as page)

enter image description here

//ES6 const, let
//ES6 Destructuring 
const { Component, useCallback, useState, useEffect } = React;

const SUBChildComponent = (props) => (<button onClick={props.getVal}>GetValue with debounce</button>);

const ChildComponent = () => {
    // some unstable states
    const [someVal1, setSomeVal1] = useState(0);
    const [someVal2, setSomeVal2] = useState(0);
    const [someVal3, setSomeVal3] = useState(0);

    // some callback witch works with states AND called from subClild components
    const getVal = useCallback(_.debounce(() => {
        alert(`${someVal1}\n${someVal2}\n${someVal3}`);
    }, 1000), [someVal1, someVal2, someVal3]);

    // some synthetic changes
    useEffect(() => {
        const id = setInterval(() => setSomeVal1(someVal1 + 1), 50);
        return () => clearInterval(id);
    }, [someVal1]);

    // some synthetic changes
    useEffect(() => {
        const id = setInterval(() => setSomeVal2(someVal2 + 1), 100);
        return () => clearInterval(id);
    }, [someVal2]);

    // some synthetic changes
    useEffect(() => {
        const id = setInterval(() => setSomeVal3(someVal3 + 1), 250);
        return () => clearInterval(id);
    }, [someVal3]);

    return <React.Fragment><SUBChildComponent getVal={getVal}/><br/>{someVal1}<br/>{someVal2}<br/>{someVal3}
    </React.Fragment>;
};

class App extends Component {
    render() {
        return (<div><ChildComponent/></div>);
    }
}

ReactDOM.render(<App/>, document.querySelector(".container"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js"></script>

<div class="container"></div>


Solution

  • First of all you must note that the debounce function sets the states from its closure when its created. Now the function is executed a few seconds later and by that time the states would have changed. Also a new instance of debounce will be created each time states are updated, so if at all you use debounce function onClick, it won't work correctly as different calls will be calling different instances of debounce function and not the same one

    The solution in such cases is to pass on the state values as argument to debounce function instead of letting it rely on the closure. It however it still use the value with which debounce was called, as you can see in below snippet

    //ES6 const, let
    //ES6 Destructuring 
    const { Component, useCallback, useState, useEffect } = React;
    
    const SUBChildComponent = ({getVal, someVal1,someVal2,someVal3}) => (<button onClick={() => getVal(someVal1,someVal2,someVal3)}>GetValue with debounce</button>);
    
    const ChildComponent = () => {
        // some unstable states
        const [someVal1, setSomeVal1] = useState(0);
        const [someVal2, setSomeVal2] = useState(0);
        const [someVal3, setSomeVal3] = useState(0);
    
        // some callback witch works with states AND called from subClild components
        const getVal = useCallback(_.debounce((val1, val2, val3) => {
            alert(`${val1}\n${val2}\n${val3}`);
        }, 1000), []); // create debounce function only once
    
        // some synthetic changes
        useEffect(() => {
            const id = setInterval(() => setSomeVal1(someVal1 + 1), 50);
            return () => clearInterval(id);
        }, [someVal1]);
    
        // some synthetic changes
        useEffect(() => {
            const id = setInterval(() => setSomeVal2(someVal2 + 1), 100);
            return () => clearInterval(id);
        }, [someVal2]);
    
        // some synthetic changes
        useEffect(() => {
            const id = setInterval(() => setSomeVal3(someVal3 + 1), 250);
            return () => clearInterval(id);
        }, [someVal3]);
    
        return <React.Fragment><SUBChildComponent someVal1={someVal1} someVal2={someVal2} someVal3={someVal3} getVal={getVal}/><br/>{someVal1}<br/>{someVal2}<br/>{someVal3}
        </React.Fragment>;
    };
    
    class App extends Component {
        render() {
            return (<div><ChildComponent/></div>);
        }
    }
    
    ReactDOM.render(<App/>, document.querySelector(".container"));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js"></script>
    
    <div class="container"></div>

    Now another solution is to keeps refs of state and use them within debounce function which is what you want in your case

    //ES6 const, let
    //ES6 Destructuring 
    const { Component, useCallback, useState, useEffect, useRef } = React;
    
    const SUBChildComponent = React.memo(({getVal}) => {
        console.log('child render');
        return <button onClick={() => getVal()}>GetValue with debounce</button>;
    });
    
    const ChildComponent = () => {
        // some unstable states
        const [someVal1, setSomeVal1] = useState(0);
        const [someVal2, setSomeVal2] = useState(0);
        const [someVal3, setSomeVal3] = useState(0);
        const someVal1Ref = useRef(someVal1);
        const someVal2Ref = useRef(someVal2);
        const someVal3Ref = useRef(someVal3);
         
        useEffect(() => {
            someVal1Ref.current = someVal1;
            someVal2Ref.current = someVal2;
            someVal3Ref.current = someVal3;
        }, [someVal1, someVal2, someVal3])
        
        // some callback witch works with states AND called from subClild components
        const getVal = useCallback(_.debounce(() => {
            alert(`${someVal1Ref.current}\n${someVal2Ref.current}\n${someVal3Ref.current}`);
        }, 1000), []); // create debounce function only once
    
        // some synthetic changes
        useEffect(() => {
            const id = setInterval(() => setSomeVal1(someVal1 + 1), 50);
            return () => clearInterval(id);
        }, [someVal1]);
    
        // some synthetic changes
        useEffect(() => {
            const id = setInterval(() => setSomeVal2(someVal2 + 1), 100);
            return () => clearInterval(id);
        }, [someVal2]);
    
        // some synthetic changes
        useEffect(() => {
            const id = setInterval(() => setSomeVal3(someVal3 + 1), 250);
            return () => clearInterval(id);
        }, [someVal3]);
    
        return <React.Fragment><SUBChildComponent getVal={getVal}/><br/>{someVal1}<br/>{someVal2}<br/>{someVal3}
        </React.Fragment>;
    };
    
    class App extends Component {
        render() {
            return (<div><ChildComponent/></div>);
        }
    }
    
    ReactDOM.render(<App/>, document.querySelector(".container"));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js"></script>
    
    <div class="container"></div>

    PS. Such kind of implementation are way easier in class components and don't need any work around as you are not dependent on closures