Async Increment at once in a React Counter using hooks

Solution 1:

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.