Nesh
Nesh

Reputation: 2561

Async Increment at once in a React Counter using hooks

Following is the code which increments the value at once after 4 sec, though I am expecting the batch of update should increment the valus after 4 sec only on multiple clicks.

Ex. - Let us say, I clicked the "Async Increase" button 5 times, then after 4 sec the counter increases to 1,2,3,4,5 but I want after 4 sec it should increment making it 1 then after 4 sec it should increment it to 2, then after 4 sec it should increase to 3 and so on.

Let me know how can I fix this.

Code -

const UseStateCounter = () => {
  const [value, setValue] = useState(0);

  const reset = () => {
    setValue(0);
  }

  const asyncIncrease = () => {
    setTimeout(() => {
      setValue(prevValue => prevValue + 1);
    }, 4000);
  }

  const asyncDecrease = () => {
    setTimeout(() => {
      setValue(prevValue => prevValue - 1);
    }, 4000);
  }

  return <>
    <section style={{margin: '4rem 0'}}>
      <h3>Counter</h3>
      <h2>{value}</h2>
      <button className='btn' onClick={asyncDecrease}>Async Decrease</button>
      <button className='btn' onClick={reset}>Reset</button>
      <button className='btn' onClick={asyncIncrease}>Async Increase</button>
    </section>
  </>
};

export default UseStateCounter;

Upvotes: 1

Views: 974

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1075567

To do that, wait for the previous change to finish before you start the next one. For instance, one way to do that is with a promise chain; see comments:

// Promise-ified version of setTimeout
const timeout = (ms) => new Promise(resolve => setTimeout(resolve, ms));

const UseStateCounter = () => {
    const [value, setValue] = useState(0);
    // Remember the promise in a ref we initialize
    // with a fulfilled promise
    const changeRef = useRef(Promise.resolve());
    /* Alternatively, if there's a lot of initialization logic
       or object construction, you might use `null` above
       and then:
    if (!changeRef.current) {
        changeRef.current = Promise.resolve();
    }
    */

    const reset = () => {
        queueValueUpdate(0, false);
    };

    // A function to do the queued update
    const queueValueUpdate = (change, isDelta = true) => {
        changeRef.current = changeRef.current
            // Wait for the previous one to complete, then
            .then(() => timeout(4000)) // Add a 4s delay
            // Then do the update
            .then(() => setValue(prevValue => isDelta ? prevValue + change : change));
    };

    const asyncIncrease = () => {
        queueValueUpdate(1);
    };

    const asyncDecrease = () => {
        queueValueUpdate(-1);
    };

    // Sadly, Stack Snippets can't handle the <>...</> form
    return <React.Fragment>
        <section style={{ margin: '4rem 0' }}>
            <h3>Counter</h3>
            <h2>{value}</h2>
            <button className='btn' onClick={asyncDecrease}>Async Decrease</button>
            <button className='btn' onClick={reset}>Reset</button>
            <button className='btn' onClick={asyncIncrease}>Async Increase</button>
        </section>
    </React.Fragment>;
};

export default UseStateCounter;

Live Example:

const {useState, useRef} = React;

// Promise-ified version of setTimeout
const timeout = (ms) => new Promise(resolve => setTimeout(resolve, ms));

const UseStateCounter = () => {
    const [value, setValue] = useState(0);
    // Remember the promise in a ref we initialize
    // with a fulfilled promise
    const changeRef = useRef(Promise.resolve());
    /* Alternatively, if there's a lot of initialization logic
       or object construction, you might use `null` above
       and then:
    if (!changeRef.current) {
        changeRef.current = Promise.resolve();
    }
    */

    const reset = () => {
        queueValueUpdate(0, false);
    };

    // A function to do the queued update
    const queueValueUpdate = (change, isDelta = true) => {
        changeRef.current = changeRef.current
            // Wait for the previous one to complete, then
            .then(() => timeout(4000)) // Add a 4s delay
            // Then do the update
            .then(() => setValue(prevValue => isDelta ? prevValue + change : change));
    };

    const asyncIncrease = () => {
        queueValueUpdate(1);
    };

    const asyncDecrease = () => {
        queueValueUpdate(-1);
    };

    // Sadly, Stack Snippets can't handle the <>...</> form
    return <React.Fragment>
        <section style={{ margin: '4rem 0' }}>
            <h3>Counter</h3>
            <h2>{value}</h2>
            <button className='btn' onClick={asyncDecrease}>Async Decrease</button>
            <button className='btn' onClick={reset}>Reset</button>
            <button className='btn' onClick={asyncIncrease}>Async Increase</button>
        </section>
    </React.Fragment>;
};

ReactDOM.render(<UseStateCounter />, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

Note: Normally I make a big noise about handling promise rejections, but none of the promise stuff above will ever reject, so I'm comfortable not bothering with catch in queueValueUpdate.

Upvotes: 3

Related Questions