2

So the timer works. If I hard code this.state with a specific countdown number, the timer begins counting down once the page loads. I want the clock to start counting down on a button click and have a function which changes the null of the state to a randomly generated number. I am a bit new to React. I am know that useState() only sets the initial value but if I am using a click event, how do I reset useState()? I have been trying to use setCountdown(ranNum) but it crashes my app. I am sure the answer is obvious but I am just not finding it.

If I didnt provide enough code, please let me know. I didn't want to post the whole shebang.

here is my code:

import React, { useState, useEffect } from 'react';

export const Timer = ({ranNum, timerComplete}) => {
    const [ countDown, setCountdown ] = useState(ranNum)
    useEffect(() => {
        setTimeout(() => {
            countDown - 1 < 0 ? timerComplete() : setCountdown(countDown - 1)
        }, 1000)
    }, [countDown, timerComplete])
    return ( <p >Countdown: <span>{ countDown }</span> </p> )
}


  handleClick(){
    let newRanNum = Math.floor(Math.random() * 20);
    this.generateStateInputs(newRanNum)
    let current = this.state.currentImg;
    let next = ++current % images.length;
    this.setState({
      currentImg: next,
      ranNum: newRanNum
    })
  }

 <Timer ranNum={this.state.ranNum} timerComplete={() => this.handleComplete()} />
 <Button onClick={this.handleClick}  name='Generate Inputs' />
 <DisplayCount name='Word Count: ' count={this.state.ranNum} />

3 Answers 3

6

You should store countDown in the parent component and pass it to the child component. In the parent component, you should use a variable to trigger when to start Timer. You can try this:

import React from "react";

export default function Timer() {
  const [initialTime, setInitialTime] = React.useState(0);
  const [startTimer, setStartTimer] = React.useState(false);

  const handleOnClick = () => {
    setInitialTime(5);
    setStartTimer(true);
  };

  React.useEffect(() => {
    if (initialTime > 0) {
      setTimeout(() => {
        console.log("startTime, ", initialTime);
        setInitialTime(initialTime - 1);
      }, 1000);
    }

    if (initialTime === 0 && startTimer) {
      console.log("done");
      setStartTimer(false);
    }
  }, [initialTime, startTimer]);

  return (
    <div>
      <buttononClick={handleOnClick}>
        Start
      </button>
      <Timer initialTime={initialTime} />
    </div>
  );
}

const Timer = ({ initialTime }) => {
  return <div>CountDown: {initialTime}</div>;
};

Sign up to request clarification or add additional context in comments.

Comments

2

useState sets the initial value just like you said, but in your case I don't think you want to store the countDown in the Timer. The reason for it is that ranNum is undefined when you start the application, and gets passed down to the Timer as undefined. When Timer mounts, useEffect will be triggered with the value undefined which is something you don't want since it will trigger the setTimeout. I believe that you can store countDown in the parent of the Timer, start the timeout when the button is clicked from the parent and send the countDown value to the Timer as a prop which would make the component way easier to understand.

Comments

1

Here is a simple implementation using hooks and setInterval

import React, {useState, useEffect, useRef} from 'react'
import './styles.css'

const STATUS = {
  STARTED: 'Started',
  STOPPED: 'Stopped',
}

export default function CountdownApp() {
  const [secondsRemaining, setSecondsRemaining] = useState(getRandomNum())
  const [status, setStatus] = useState(STATUS.STOPPED)

  const handleStart = () => {
    setStatus(STATUS.STARTED)
  }
  const handleStop = () => {
    setStatus(STATUS.STOPPED)
  }
  const handleRandom = () => {
    setStatus(STATUS.STOPPED)
    setSecondsRemaining(getRandomNum())
  }
  useInterval(
    () => {
      if (secondsRemaining > 0) {
        setSecondsRemaining(secondsRemaining - 1)
      } else {
        setStatus(STATUS.STOPPED)
      }
    },
    status === STATUS.STARTED ? 1000 : null,
    // passing null stops the interval
  )
  return (
    <div className="App">
      <h1>React Countdown Demo</h1>
      <button onClick={handleStart} type="button">
        Start
      </button>
      <button onClick={handleStop} type="button">
        Stop
      </button>
      <button onClick={handleRandom} type="button">
        Random
      </button>
      <div style={{padding: 20}}>{secondsRemaining}</div>
      <div>Status: {status}</div>
    </div>
  )
}

function getRandomNum() {
  return Math.floor(Math.random() * 20)
}

// source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
function useInterval(callback, delay) {
  const savedCallback = useRef()

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback
  }, [callback])

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current()
    }
    if (delay !== null) {
      let id = setInterval(tick, delay)
      return () => clearInterval(id)
    }
  }, [delay])
}

Here is a link to a codesandbox demo: https://codesandbox.io/s/react-countdown-demo-random-c9dm8?file=/src/App.js

Comments

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.