1
votes

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

3 Answers

3
votes

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>;
};
2
votes

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.

0
votes

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