0

I try to add a feature to my simple Counter (React) App which alerts when counter reaches 0 onClick increase or decrease button. But alert always late for 1 click. How can I fix it? Here is my code:

function App() {
  const [counter, setCounter] = useState(0);

  function handleIncrement() {
    setCounter((oldState) => oldState + 1);
    if (counter === 0) alert('it is 0');
  }

  function handleDecrement() {
    setCounter((oldState) => oldState - 1);
    if (counter === 0) alert('it is 0');
  }

  return (
    <div>
      <button onClick={handleIncrement}>increment</button>
      <div>{counter}</div>
      <button onClick={handleDecrement}>decrement</button>
    </div>
  );
}

I want to see alert exactly when I see 0 on the screen. But the code above shown alert only after the counter passed zero.

Elchin
  • 3
  • 3

4 Answers4

1

This is happening bcoz setCounter is async operation. We can fix this using this two ways

  1. wrap alert function inside the setCounter's callback.
function handleIncrement() {
    
  setCounter((oldState) => {
    
    if (oldState + 1 === 0) {
      alert('it is');
    }
    
  return oldState + 1;
  });  
}
    
    
function handleDecrement() {
    
  setCounter((oldState) => {
    
    if (oldState - 1 === 0) {
      alert('it is 0');
    }
    
    return oldState - 1;
    
  });
}
  1. you can also use useEffect to achieve this
useEffect(() => {
  if (counter === 0) {
    alert('it is 0');
  }

}, [counter]);

    
function handleIncrement() {
  setCounter(counter + 1);
}

    
function handleDecrement() {
  setCounter(counter - 1);
}
derFrosty
  • 550
  • 3
  • 17
Lalit Tyagi
  • 264
  • 2
  • 10
0

You can use useEffect, and a ref. The ref stores whether it's the first update or not. The useEffect checks to see if it's the first update. If it is it resets the ref to false, otherwise it logs/alerts the counter state.

(Cribbed slightly from this question.)

const { useEffect, useState, useRef } = React;

function Example() {

  const [counter, setCounter] = useState(0);

  const firstUpdate = useRef(true);

  function handleIncrement() {
    setCounter(prev => prev + 1);
  }

  function handleDecrement() {
    setCounter(prev => prev - 1);
  }

  useEffect(() => {
    if (firstUpdate.current) {
      firstUpdate.current = false;
      return;
    }
    if (counter === 0) console.log('it is');
  }, [counter]);

  return (
    <div>
      <button onClick={handleIncrement}>increment</button>
      <div>{counter}</div>
      <button onClick={handleDecrement}>decrement</button>
    </div>
  );

}

ReactDOM.render(
  <Example />,
  document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Andy
  • 61,948
  • 13
  • 68
  • 95
0

that's okay, because react renders in frames like when you watch a movie or play a video game, at the current frame where you call this line

    setCounter((oldState) => oldState - 1);

counter still 1 not 0, so you need to wait until react do the re-render, then the next render start with counter being 0, but the alert you defined inside click handler won't be called automatically, you didn't call it, you made that call inside the onClick handler which will not be called until button clicked(obviously)

so the problem is not with how react re-render the state, it's where to put your alert or in other words how to consume your state

a simple fix to your solution is to put alert outside the call, in the component body

function App() {
  const [counter, setCounter] = useState(0);

  function handleIncrement() {
    setCounter((oldState) => oldState + 1);
  }

  function handleDecrement() {
    setCounter((oldState) => oldState - 1);
  }

  if (counter === 0) alert('it is 0');


  return (
    <div>
      <button onClick={handleIncrement}>increment</button>
      <div>{counter}</div>
      <button onClick={handleDecrement}>decrement</button>
    </div>
  );
}

Anyway it's not a good idea to have alert called inside the body of a component so I'd assume you have an alert component that's shown to the user when the counter reach 0

function App() {
  const [counter, setCounter] = useState(0);

  function handleIncrement() {
    setCounter((oldState) => oldState + 1);
  }

  function handleDecrement() {
    setCounter((oldState) => oldState - 1);
  }

  if(counter == 0){
    return <Alert message="counter is 0 now"/>
  }

  return (
    <div>
      <button onClick={handleIncrement}>increment</button>
      <div>{counter}</div>
      <button onClick={handleDecrement}>decrement</button>
    </div>
  );
}

Louay Al-osh
  • 3,177
  • 15
  • 28
0

The best way for solving your problem is to handle states update inside the useEffect hook. Your issue happened because of useState async hook.

import { useEffect, useState } from "react";
import "./styles.css";

export default function App() {
const [counter, setCounter] = useState(0);

function handleIncrement() {
  setCounter((oldState) => oldState + 1);
}

function handleDecrement() {
  setCounter((oldState) => oldState - 1);
}

useEffect(()=>{
  if(counter === 0) {
    alert('it is')
  }
}, [counter])

return (
  <div>
    <button onClick={handleIncrement}>increment</button>
    <div>{counter}</div>
    <button onClick={handleDecrement}>decrement</button>
  </div>
 );
}