Search code examples
javascriptreactjsreact-componentreact-functional-componentreact-class-based-component

How to implement global variable in react functional components


I am new to react and decided to practice by implementing a simple stop watch using both class and functional components.

I successfully implemented the stop watch using a class component. Below is the code:

Class Component


class Stopwatch extends Component {
  state = {
    status: false,
    ms: 0,
    seconds: 0,
    minutes: 0,
  };

  stopms;
  stopSeconds;
  stopMinutes;

  handleClick = () => {
    this.changeStatus();
    if (this.state.status) {
      clearInterval(this.stopms);
      clearInterval(this.stopSeconds);
      clearInterval(this.stopMinutes);
    } else {
      this.stopms = setInterval(this.changeMs, 1);
      this.stopSeconds = setInterval(this.changeSeconds, 1000);
      this.stopMinutes = setInterval(this.changeMinutes, 60000);
    }
  };

  changeStatus = () => {
    return this.setState((state) => {
      return { status: !state.status };
    });
  };

  changeMs = () => {
    return this.setState((state) => {
      if (state.ms === 99) {
        return { ms: 0 };
      } else {
        return { ms: state.ms + 1 };
      }
    });
  };

  changeSeconds = () => {
    return this.setState((state) => {
      if (state.seconds === 59) {
        return { seconds: 0 };
      } else {
        return { seconds: state.seconds + 1 };
      }
    });
  };

  changeMinutes = () => {
    return this.setState((state) => {
      if (state.seconds === 59) {
        return { minutes: 0 };
      } else {
        return { minutes: state.minutes + 1 };
      }
    });
  };

  handleReset = () => {
    clearInterval(this.stopms);
    clearInterval(this.stopSeconds);
    clearInterval(this.stopMinutes);
    this.setState({ seconds: 0, status: false, minutes: 0, ms: 0 });
  };

  componentWillUnmount() {
    clearInterval(this.stopms);
    clearInterval(this.stopSeconds);
    clearInterval(this.stopMinutes);
  }

  render() {
    return (
      <div>
        <h1>
          {this.state.minutes} : {this.state.seconds} .{" "}
          <span>{this.state.ms}</span>
        </h1>
        <button className="btn btn-lg btn-dark" onClick={this.handleClick}>
          {this.state.status === false ? "Start" : "Pause"}
        </button>
        <button className="btn btn-lg btn-dark" onClick={this.handleReset}>
          Reset
        </button>
      </div>
    );
  }
}

export default Stopwatch;

Now I'm trying to implement the same code above but using a functional component as shown below:

Functional Component


function Stopwatch() {
  const [timeState, setTimeState] = useState({
    status: false,
    ms: 0,
    seconds: 0,
    minutes: 0,
  });

  let stopms;
  let stopSeconds;
  let stopMinutes;

  const handleClick = () => {
    changeStatus();
    if (timeState.status) {
      clearInterval(stopms);
      clearInterval(stopSeconds);
      clearInterval(stopMinutes);
    } else {
      stopms = setInterval(changeMs, 1);
      stopSeconds = setInterval(changeSeconds, 1000);
      stopMinutes = setInterval(changeMinutes, 60000);
    }
  };

  const changeStatus = () => {
    return setTimeState((prevState) => {
      return { ...prevState, status: !prevState.status };
    });
  };

  const changeMs = () => {
    return setTimeState((prevState) => {
      if (prevState.ms === 99) {
        return { ...prevState, ms: 0 };
      } else {
        return { ...prevState, ms: prevState.ms + 1 };
      }
    });
  };

  const changeSeconds = () => {
    return setTimeState((prevState) => {
      if (prevState.seconds === 59) {
        return { ...prevState, seconds: 0 };
      } else {
        return { ...prevState, seconds: prevState.seconds + 1 };
      }
    });
  };

  const changeMinutes = () => {
    return setTimeState((prevState) => {
      if (prevState.seconds === 59) {
        return { ...prevState, minutes: 0 };
      } else {
        return { ...prevState, minutes: prevState.minutes + 1 };
      }
    });
  };

  const handleReset = () => {
    clearInterval(stopms);
    clearInterval(stopSeconds);
    clearInterval(stopMinutes);
    setTimeState({ seconds: 0, status: false, minutes: 0, ms: 0 });
  };

  return (
    <div>
      <h1>
        {timeState.minutes} : {timeState.seconds} . <span>{timeState.ms}</span>
      </h1>
      <button className="btn btn-lg btn-dark" onClick={handleClick}>
        {timeState.status === false ? "Start" : "Stop"}
      </button>
      <button className="btn btn-lg btn-dark" onClick={handleReset}>
        Reset
      </button>
    </div>
  );
}

export default Stopwatch;

The Problem

In the class component, I implemented the "Pause" functionality using the handleClick function which calls clearInterval with it's argument as the global variables stopms, stopSeconds, stopMinutes that I declared initially. This worked just fine because these global variables were holding values returned from the respective setInterval when the stop watch started counting.

Now in the functional component, I replicated the same logic by declaring the same global variables using the "let" keyword. But the "Pause" functionality is not working. When the "Start" button hit and the handleClick function called, the setIntervals were called and their return values were stored in the respective global variables. But when the "Pause" button was hit, all the global variables had "undefined" as their values.

Please I would like to know if there's any other way I can declare global variables and use them to hold values throughout a component's life cycle asides using state.


Solution

  • Functional components are executed from top to bottom whenever state changes, so the whole function is re-executed and that's how it returns the new JSX, compare this to class components where only render() function is executed on render, that's how functional components work.

    The problem is that your global variables are in fact not global and a part of the function, hence they are re-initialized each time render is happening.

    Two ways to solve this

    Move your variables to the state

    function Stopwatch() {
      const [timeState, setTimeState] = useState({
        status: false,
        ms: 0,
        seconds: 0,
        minutes: 0,
        stopms : null,
        stopSeconds : null,
        stopMinutes: null,
      });
    
      const handleClick = () => {
        changeStatus();
        if (timeState.status) {
          clearInterval(timeState.stopms);
          clearInterval(timeState.stopSeconds);
          clearInterval(timeState.stopMinutes);
        } else {
          let stopms = setInterval(changeMs, 1);
          let stopSeconds = setInterval(changeSeconds, 1000);
          let stopMinutes = setInterval(changeMinutes, 60000);
    
          setTimeState(prev => ({..prev, stopms, stopSeconds, stopMinutes})); // update the values in state
        }
      };
    
       ...... 
    
       const handleReset = () => {
        clearInterval(timeState.stopms); // use the same values to clear them
        clearInterval(timeState.stopSeconds);
        clearInterval(timeState.stopMinutes);
        .....
      };
    
     ..... 
    
     } 
    

    Or make them global by placing them outside of your component, Will work but not recommended.

    In your component file.

     // declare them just above your function
     let stopms;
     let stopSeconds;
     let stopMinutes;
      
     function Stopwatch() {
      const [timeState, setTimeState] = useState({
        status: false,
        ms: 0,
        seconds: 0,
        minutes: 0,
      });
      .....
    
      const handleClick = () => {
        changeStatus();
        if (timeState.status) {
          clearInterval(stopms);
          clearInterval(stopSeconds);
          clearInterval(stopMinutes);
        } else {
          stopms = setInterval(changeMs, 1);
          stopSeconds = setInterval(changeSeconds, 1000);
          stopMinutes = setInterval(changeMinutes, 60000);
        }
       .......
      };