4

I want build a Circular ProgressBar that count at 60 and then automatically stop.

But it can't stop.

I want use React hooks and useEffect

My code here:

https://codesandbox.io/s/nostalgic-khorana-lijdyo?file=/src/App.js:0-686

But the code essence here also:

import React, { useState, useEffect } from "react";
import Circle from "./Circle";
export default function App() {
  const [counter, setCounter] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      if (counter < 60) {
        setCounter((t) => t + 1);
        console.log(counter);
      } else {
        console.log(`Why not run?`);
        return () => clearInterval(intervalId);
      }
      return () => clearInterval(intervalId);
    }, 100);
  }, []);

  return (
    <div>
      <div>
        <Circle
          size={250}
          strokeWidth={5}
          percentage={counter}
          color="#229880"
        />
      </div>
    </div>
  );
}

3 Answers 3

5

Since you do not include counter in the dependency of useEffect, if (counter < 60) {...} statement will always be true (since the counter is equal to 0 in every re-render in react). The easiest way to get the previous value of the counter would be acquire it in the setCounter function.

useEffect(() => {
  const intervalId = setInterval(() => {
    setCounter((t) => {
      if (t >= 60) clearInterval(intervalId);
      return (t < 60) ? t + 1 : t;
    });
  }, 100);
  return () => clearInterval(intervalId);
}, []);
Sign up to request clarification or add additional context in comments.

2 Comments

I never considered a situation where the state setter function performs side effects. Is that considered good practice in React? If so, I think this would be a better solution than my answer because the decision of whether or not to stop the timer is done using a more up to date value of counter.
@Rotem, no I don't think my answer is a good practice in React. It's just like a workaround in this case. The common way would be to use the useInterval hook.
3

Here is my solution for this

 const [counter, setCounter] = useState(0);
  const counterValid = counter < 60;
  useEffect(() => {
    const intervalId = counterValid && setInterval(() => 
        setCounter((t) => t + 1)
      , 100);
      return () => clearInterval(intervalId)
  }, [counterValid]);

  return (
    <div>
      <div>
        <Circle
          size={250}
          strokeWidth={5}
          percentage={counter*(100/60)}
          color="#229880"
        />
      </div>
    </div>

We add counterValid as a dependency to useEffect to re-run the effect whenever the validity of the counter changes.

Also note that your circle expects a 1-100 value for percentage so I multiplied it by 100/60.

2 Comments

Because of the counterValid resulting in a boolean value, Typescript compiler will complain that the intervalId is not assignable to type number. Can you think of a nicer solution without casting to any?
Couldn't you just use a ternary conditional assignment instead?
0

Since you are using counter within your hook, you should include counter as a dependency of your useEffect. In addition, you may need to maintain a state for intervalId so you could add another state variable for that.

For example:

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

  useEffect(() => {
    // Only call the setInterval if it is not set already.
    if (intervalId) {
      return;
    }
    const newIntervalId = setInterval(() => {
      if (counter < 60) {
        // You may also be incorrectly setting counter, try just incrementing...
        setCounter(counter + 1);
        console.log(counter);
      } else {
        console.log(`Why not run?`);
        clearInterval(intervalId);
      }
    }, 100);
    setIntervalId(newIntervalId);

    // You are also returning the callback to clean up the effect in the wrong place.
   // I moved this outside the interval callback for React to have a way to clear the interval during component unmount cycle.
    return () => clearInterval(intervalId);
  }, [ counter, intervalId ]);

EDIT

There is probably another way to achieve what you are doing, what I introduced may be excessive. The issue may actually be your incrementing logic, I would try just updating that first:

// Change this
setCounter((t) => t + 1);

// To this
setCounter(counter + 1);

1 Comment

Thank you your effort but it not works at all. The counter not start also...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.