5

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>
mixalbl4
  • 3,507
  • 1
  • 30
  • 44
  • Does this answer your question? [React - useState - why setTimeout function does not have latest state value?](https://stackoverflow.com/questions/55198517/react-usestate-why-settimeout-function-does-not-have-latest-state-value) – JDansercoer May 19 '20 at 08:58
  • I don't understand why you debouce the getter. Wouldn't it make much more sense to debounce setting the state and simply pass the current state down as props? – trixn May 19 '20 at 09:07
  • @trixn, nope. This is a library and I just give `save()` method for users code. Users code doesnt know about any library private states and theirs values. They just made component with `` and called `save()` a lot of times. @JDansercoer nope, this is a similar problem but not the same, and solutions is totally different. – mixalbl4 May 19 '20 at 09:18
  • @MixerOID So in your real application `getVal` is `save` and instead of `altert`ing you want to save the current values from your state to your backend (which triggers an api call)? – trixn May 19 '20 at 09:24
  • @trixn, yep. This code is just "simplified example" for showing the problem in a simplest way. – mixalbl4 May 19 '20 at 09:27

1 Answers1

8

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

Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • Good solution with `ref`. As I see you can remove `someVal1={someVal1} someVal2={someVal2} someVal3={someVal3}` in final code. But why `SUBChildComponent` is re-renders every time? me and you used `useCallback` for props, I expected to avoid a sub-component renderer. – mixalbl4 May 19 '20 at 09:14
  • That you can achieve by using `React.memo` – Shubham Khatri May 19 '20 at 09:18
  • reactjs.org docs says that `useCallback(fn, deps) is equivalent to useMemo(() => fn, deps)`. But as I see its not the same. I thought that by default, using static / memoized props in functional components, I also prevent re-rendering as with conventional components. But apparently this is not so. thank – mixalbl4 May 19 '20 at 09:25
  • 1
    They can be used used for the same purpose but have different syntaxes. However to prevent re-rendering you need `React.memo` along with usecallback to memoize the function – Shubham Khatri May 19 '20 at 09:26
  • arigato! senpai – Aashutosh Rathi Sep 13 '22 at 11:43