ludinj
ludinj

Reputation: 115

How to set the state in react with a delay in a for loop

i want to set a state in react in a loop going from 0 to "last step" but the state has to be set with a delay in other words:

-click a button, after the delay the state is set to 0, again after the delay the state is set to previous state +1, once the state is equal to "last step" end.

i tried this but is not working

const handleReplay = () => {
   setTimeout(() => {
     for (let i = 0; i < lastStep; i++) {
       setStepNumber(i)
     }
   }, 500);
  };

Upvotes: 0

Views: 646

Answers (3)

Yanick Rochon
Yanick Rochon

Reputation: 53616

If the button starts a counter from 0 to lastStep, then you need more than useState:

Solution 1: states and effect

import { useEffect, useState } from "react";

// global constants:
const INIT_STEP = -1;
const LAST_STEP = 9; // for steps 0...9 (10 steps)
const DELAY = 1000;

export default () => {
  // not active by default
  const [active, setActive] = useState(false);
  const [step, setStep] = useState(INIT_STEP);

  useEffect(() => {
    // if we are active, then start the incrementing timer
    if (active) {
      // set the internal step state
      let currentStep = INIT_STEP;

      // create an interval which will increment the step
      const timer = setInterval(() => {
        if (currentStep < LAST_STEP) {
          currentStep = currentStep + 1;
          setStep(currentStep);
        } else {
          // stop here because we have reached the end of steps
          setActive(false);
        }
      }, DELAY);

      // will be called when active is set to false
      return () => clearInterval(timer);
    }
  }, [active]);

  // external control
  const handleButtonStart = () => {
    setStep(INIT_STEP);
    setActive(true);
  };
  const handleButtonStop = () => setActive(false);

  return (
    <div>
      <h3>Solution 1: states and effect!</h3>

      <div>
        {active ? (step > INIT_STEP ? `Step ${step}` : "Pending") : "Stopped"}
      </div>

      <button onClick={handleButtonStart}>Start</button>
      <button onClick={handleButtonStop}>Stop</button>
    </div>
  );
};

Solution 2: reducer

import { useEffect, useReducer } from "react";

const INIT_STEP = -1;
const LAST_STEP = 9; // for steps 0...9 (10 steps)
const DELAY = 1000;

const INIT_STATE = {
  step: INIT_STEP,
  active: false
};

const counterReducer = (state, action) => {
  if (action.type === "start") {
    return { step: INIT_STEP, active: true };
  } else if (action.type === "stop") {
    return { ...state, active: false };
  } else if (action.type === "next" && state.active) {
    if (state.step < LAST_STEP) {
      return { ...state, step: state.step + 1 };
    } else {
      return { ...state, active: false };
    }
  } else {
    return state; // no change
  }
};

export default () => {
  const [{ step, active }, dispatch] = useReducer(counterReducer, INIT_STATE);

  useEffect(() => {
    if (active) {
      const timer = setInterval(() => {
        dispatch({ type: "next" });
      }, DELAY);

      return () => clearInterval(timer);
    }
  }, [active]);

  const handleButtonStart = () => dispatch({ type: "start" });
  const handleButtonStop = () => dispatch({ type: "stop" });

  return (
    <div>
      <h3>Solution 2: reducer!</h3>
      <div>
        {active ? (step > INIT_STEP ? `Step ${step}` : "Pending") : "Stopped"}
      </div>

      <button onClick={handleButtonStart}>Start</button>
      <button onClick={handleButtonStop}>Stop</button>
    </div>
  );
};

The code can be tested here.

Upvotes: 1

AlienWithPizza
AlienWithPizza

Reputation: 446

There is more to it, but first clue is you are waiting a timeout and then running a loop. Instead, you're going to wait a timeout and then increment state, repeat.

This is one way that should get it done.

const timeoutRef = useRef();
const [stepNumber, setStepNumber] = useState(undefined);

useEffect(() => {
    return () => {
        // On unmount, stop any current animation
        if (timeoutRef.current) {
            clearTimeout(timeoutRef.current);
        }
    }
}, []);

const onClick = useCallback(() => {
    let iStepNumber = 0;
    
    const incrementStepNumber = () => {
        setStepNumber(iStepNumber++);

        if (iStepNumber === 100 /* The last step */) {
            timeoutRef.current = undefined;
            return;
        }

        timeoutRef.current = setTimeout(() => {
            incrementStepNumber();
        }, 500);
    };

    timeoutRef.current = setTimeout(() => {
        incrementStepNumber();
    }, 500);
}, []);

Upvotes: 0

Tuan Luu
Tuan Luu

Reputation: 69

The for loop will run from start to end. if you only want to execute code on a click event then just use:

const [stepNumber, setStepNumber] = useState(0);
  const handleReplay = () => {
    setTimeout(() => {
      if (stepNumber < lastStep) {
        setStepNumber((prevNum) => prevNum + 1);
      }
    }, 500);
  };

Upvotes: 0

Related Questions