React Automatic TrafficLight

Viewed 187

I'm trying to build a traffic light that changes every 5 seconds using react and states.

So it works perfectly the first run through, but then starts overlapping and spazzing out after that. I know why, my setInterval is being called multiple times.

My question is, how can I get it to loop through just one at a time, forever?

Code in question:

import { useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';

const useStyles = makeStyles(() => ({
  greenBg: {
    backgroundColor: "#acffa1",
    height: "1000px",
    color: "green",
    display: "flex",
    alignItems: "center",
    flexDirection: "column"
  },
  yellowBg: {
    backgroundColor: "#fbff82",
    height: "1000px",
    color: "#b6c700",
    display: "flex",
    alignItems: "center",
    flexDirection: "column"
  },
  redBg: {
    backgroundColor: "#ff5757",
    height: "1000px",
    color: "#a30000",
    display: "flex",
    alignItems: "center",
    flexDirection: "column"
  },
  trafficlight: {
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "white",
    position: "absolute",
    top: "25%",
    border: "1px solid black",
    width: "180px",
    height: "350px",
    borderRadius: "10px"
  },
  text: {
    fontSize: "50px",
    position: "absolute",
    top: "85%",
  },
  lights: {
    marginTop: "20%"
  },
  light: {
    border: "1px solid lightgrey",
    borderRadius: "50px",
    width: "50px",
    height: "50px",
    marginBottom: "40px",
    backgroundColor: "lightgrey"
  },
  greenLight: {
    border: "1px solid lightgrey",
    borderRadius: "50px",
    width: "50px",
    height: "50px",
    marginBottom: "40px",
    backgroundColor: "green",
  },
  yellowLight: {
    border: "1px solid lightgrey",
    borderRadius: "50px",
    width: "50px",
    height: "50px",
    marginBottom: "40px",
    backgroundColor: "yellow"
  },
  redLight: {
    border: "1px solid lightgrey",
    borderRadius: "50px",
    width: "50px",
    height: "50px",
    marginBottom: "40px",
    backgroundColor: "red"
  },
}));

function TrafficLight() {
  const classes = useStyles();
  const [green, setGreen] = useState(classes.greenLight)
  const [yellow, setYellow] = useState(classes.light)
  const [red, setRed] = useState(classes.light)

  const [bgColor, setBgColor] = useState(classes.greenBg)
  const [color, setColor] = useState("green")

  setInterval(function () {
    if (green === classes.greenLight) {
      setYellow(classes.yellowLight)
      setBgColor(classes.yellowBg)
      setColor("yellow")
      setGreen(classes.light)
      setRed(classes.light)
      return
    }

    if (yellow === classes.yellowLight) {
      setRed(classes.redLight)
      setBgColor(classes.redBg)
      setColor("red")
      setYellow(classes.light)
      setGreen(classes.light)
      return
    }

    if (red === classes.redLight) {
      setGreen(classes.greenLight)
      setBgColor(classes.greenBg)
      setColor("green")
      setYellow(classes.light)
      setRed(classes.light)
      return
    }
  }, 1000);

  return (
    <div className={bgColor}>
      <div className={classes.trafficlight}>
        <div className={classes.lights}>
          <div className={red}></div>
          <div className={yellow}></div>
          <div className={green}></div>
        </div>
      </div>
      <div className={classes.text}>
        The light is {color}.
      </div>
    </div>
  );
}

export default TrafficLight;
2 Answers

The problem is, every time your app renders TrafficLight(), it registers a new periodic timer, so the more times your component is re-rendered, the more timers are running simultaneously, all modifying the component state.

EDIT: Actually, I've realized a zero-dependency effect hook will not work in your case, because the callback to setInterval does depend on the state of the component. So you do need to pass its dependencies to the effect hook. This way, each time the state is updated, your callback will also be updated so that it performs the proper behavior. With this solution, it is also vital to have a cleanup for the effect - this is what will prevent multiple timers from running at the same time.

Updated Code

function TrafficLight() {
  // ... Your code
  
  useEffect(() => {
    const intervalHandle = setInterval( /** Your callback code */ )
    
    // This will cancel the previous timer each time the component is re-rendered due to changing any of 
    // the passed dependencies.
    return function cleanup() {
      clearInterval(intervalHandle)
    }
  }, [red, green, yellow])

  // Your code
}

Previous Answer: The solution is to use a zero-dependency effect hook. Such a hook will run the code given to it only when your component is initially set. It's also a good idea to cancel the timer when the component is unmounted, which is done by returning a callback from the effect hook. The result should look something like this:

function TrafficLight() {
  // ... Your code
  
  useEffect(() => {
    const intervalHandle = setInterval( /** Your callback code */ )
    
    // This will cancel the timer after the component unmounts.
    return function cleanup() {
      clearInterval(intervalHandle)
    }
  }, [])

  // Your code
}

You should set the interval function inside of a useEffect method with an empty dependency array, so it only runs once when the component mounts.

As it is now, I believe that the setInterval is being called every time the state changes and the component re-renders.

Related