DEV Community

Cover image for How To: Start & Stop Counter in React
Adriana DiPietro
Adriana DiPietro

Posted on

How To: Start & Stop Counter in React

Today we will be implementing a counter in React with "start" and "stop" button functionality.

Let's look at our goals...

Goals

  1. Create a button with the inner text of "start".
  2. Create a button with the inner text of "stop".
  3. Create on-click functionality on both buttons using React event listeners and event handlers.
  4. Once the "start" button is clicked, a counter displays on the page and begin increasing by increments of 1 second.
  5. Once the "start" button is clicked, a new button of "pause" is displayed on the browser.
  6. Create on-click functionality on the pause button using event listeners to pause the counter and change the inner text of the button to "resume".
  7. Once the "resume" button is clicked, the counter will unpause and the inner text will change back to "pause".
  8. Once the "stop" button is clicked, the counter will stop and be removed from the page.
  9. Once the "stop" button is clicked, the "pause" or "resume" button will be removed from the page.
  10. The "start" button should be disabled if it has been pressed.
  11. The "stop" button should be disabled if it has been pressed.

Before we dive into achieving our goals, let's think about some key concepts or questions we need to approach...

Key Concepts + Questions

  • How does React handle events?
  • How does JS handle events?
  • What is a user event?
  • How do we ensure HTML will be rendered to the page?
  • How do we increment time by seconds?
  • How do we mount or unmount HTML elements from the page?
  • What attributes do button elements need?
  • How does the browser know when an user event occurs?

With these concepts in mind, let's begin!

Start + Stop Button

For this example, I am building a React functional component named "Counter".

import React from 'react'

export default function Timer() {
    return (
        <div className="counter-container">

        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

In whatever React component you want to have your counter functionality, you can build two (2) button elements in the return statement inside a div. You may choose to classify the div element with a class name as such "counter-container" or "counter-buttons".

** Remember: return statements can only return a single parent element, so it is best to nest children elements in a div or a fragment. **

import React from 'react'

export default function Timer() {
    return (
        <div className="counter-container">
            <button className="start-button">start</button>
            <button className="stop-button">stop</button>
        </div>
    )
}

Enter fullscreen mode Exit fullscreen mode

I then assigned a class name to each button element. This will help later on to differentiate between buttons. I also give each button inner text that will be displayed to the browser.

Creating onClick Event Listeners & Handlers for Start + Stop

Now we can add event listeners to each button. It is standard to use camelCase for any attributes nested inside an html element in React. We will pass a function into each event listener, which we will both define and invoke later.

import React from 'react'

export default function Timer() {
    const startTimer = () => {

    }

    const stopTimer = () => {

    }

    return (
        <div className="counter-container">
            <button className="start-button" onClick={startTimer}>start</button>
            <button className="stop-button" onClick={stopTimer}>stop</button>
        </div>
    )
}

Enter fullscreen mode Exit fullscreen mode

Above, we set "onClick" event listener attributes and pass in
functions. We then declare arrow functions with the function name we passed into our event listeners for both the "start" and "stop" buttons.

Our browser should look something like this now:
Screen Shot 2021-08-30 at 2.18.04 PM

But if you click either button, you will notice nothing will happen because we have not defined our event handlers (startTimer + stopTimer) yet!

Using React State + Hook To Set an Interval

Next, since we are using React and JavaScript, and we are in a React functional component, we must utilize lifecycle hooks to set and manipulate state!

import React from 'react'
import { useState } from 'react'

export default function Timer() {
    const [seconds, setSeconds] = useState(0)


    const startTimer = () => {

    }

    const stopTimer = () => {

    }

    const currentCount = seconds

    return (

        <div className="counter-container">
            <button className="start-button" onClick={startTimer}>start</button>
            <button className="stop-button" onClick={stopTimer}>stop</button>
            <p id="counter">{currentCount}</p>
        </div>
    )
}

Enter fullscreen mode Exit fullscreen mode

First, I import the "useState" lifecycle hook from React at the top of our component. Then, I use destructuring to create both a "seconds" variable and a "setSeconds" function. Remember we want our counter to increase by 1 in seconds! I set both variables to the useState lifecycle hook which is a function and I pass in "0" (which represents what number our counter will start at!)

Right above the return statement, I declare and define a constant "currentCount" to our constant seconds we defined with the lifecycle hook (this is optional -- I like to name it to reflect what it will represent on the page -- hence currentCount). In the return statement, I created another html element, a p tag with an id of "counter". I then pass in the constant currentCount. This will ensure our counter will show on the page!

Our browser should look like this:
Screen Shot 2021-08-30 at 6.28.25 PM

Using setInterval() to Start the Counter

    const [seconds, setSeconds] = useState(0)

    const startTimer = () => {
            setInterval(() => {
                setSeconds(seconds => seconds + 1)
            }, 1000)
    }

    const stopTimer = () => {
        clearInterval(setSeconds(0))
        document.querySelector('#counter').remove()
    }
Enter fullscreen mode Exit fullscreen mode

In our event handler/arrow function "startTimer()", I call on setInterval(); a method that calls on another function at specified intervals (in milliseconds). setInterval(), in our case, takes an anonymous function and passes in our "setSeconds()" function that we declared using destructuring. We then pass our "seconds" variable to return the value of "seconds" incremented by 1. Finally, since setInterval() uses milliseconds, we pass in a final argument of "1000" milliseconds which equates to 1 second.

In our other event handler "stopTimer()", I call on a function called "clearInterval()". You can think of this function as the opposite cousin of "setInterval()" whereas setInterval() starts the timer, clearInterval() clears it. I then pass our function "setSeconds()" to clearInterval and pass in a zero. This will render a zero to our counter when the stop button is clicked -- starting the counter over. We then use a query selector to search our whole document for an element with an id of "counter" and remove it after. Together, this will ultimately reset the timer and remove the counter from the browser.

Pause + Resume Button, Disabling Buttons Once Clicked

const startTimer = () => {

            setInterval(() => {
                setSeconds(seconds => seconds + 1)
            }, 1000)

            document.querySelector('.start-button').setAttribute("disabled", "true")
            document.querySelector('.stop-button').removeAttribute("disabled")


                const pauseButton = document.createElement("button")
                pauseButton.innerText = 'pause'
                pauseButton.className="pause-button"
                document.querySelector('.counter-container').appendChild(pauseButton)
                pauseButton.addEventListener("click", () => {
                    if (pauseButton.innerText === "pause"){
                        pauseButton.innerText = "resume"
                    } else {
                        pauseButton.innerText = 'pause'
                    }
                })


    }

Enter fullscreen mode Exit fullscreen mode

Since we want a "pause" button to appear only once the start button has been clicked, I create and append a pause button to the page in our startTimer() event handler. I use JavaScript DOM manipulation methods to create, set the inner text, set the className and append it to our "counter-container" div.

Just above that, I use a query selector to find a button by className and use a setAttribute() method to disable the start button if clicked. On the other hand, if the start button is clicked, I make sure to remove the "disabled" attribute from the stop button. We only want start to be disabled when stop is not disabled!

I am able to change the inner text of the "pause" button to "resume" by adding a JS event listener which takes in the event type "click" and an anonymous function. Using an if-else statement, I ask if the button's inner text is equal to "pause" when clicked? If so, change the inner text to "resume"!

    const stopTimer = () => {
        clearInterval(setSeconds(0))

        if (!!document.querySelector('#counter')){
            document.querySelector('#counter').remove()
        }
        document.querySelector('.stop-button').setAttribute("disabled", "true")
        document.querySelector('.start-button').removeAttribute("disabled")
        document.querySelector('.pause-button').remove()
    }
Enter fullscreen mode Exit fullscreen mode

Now in our stopTimer(), I also use JavaScript query selector to set an attribute of "disabled" to the stop button and I remove the disabled attribute from the start button. I also remove the HTML element with an id of "counter" from the page if it already exists and I remove the pause button once the stop button is clicked.

Looking Back at Our Goals

Looking back we have achieved almost everything, we just need to achieve a few more things:

  1. Create on-click functionality on the pause button using event listeners to pause the counter.
  2. Once the "resume" button is clicked, the counter will unpause.

Pausing + Unpausing Our Counter

const [seconds, setSeconds] = useState(0)
    const [paused, setPaused] = useState(false)



    const startTimer = () => {

            const current = setInterval(() => {
                setSeconds(seconds => seconds + 1)
            }, 1000)

            document.querySelector('.start-button').setAttribute("disabled", "true")
            document.querySelector('.stop-button').removeAttribute("disabled")


                const pauseButton = document.createElement("button")
                pauseButton.innerText = 'pause'
                pauseButton.className="pause-button"
                document.querySelector('.counter-container').appendChild(pauseButton)
                pauseButton.addEventListener("click", () => {
                    if (pauseButton.innerText === "pause"){
                        pauseButton.innerText = "resume"
                        clearInterval(current)
                        setPaused(true)
                    } else {
                        pauseButton.innerText = 'pause'
                        setInterval(() => {
                            setSeconds(seconds => seconds + 1)
                        }, 1000)
                        setPaused(false)
                    }
                })


    }


Enter fullscreen mode Exit fullscreen mode

Inside of our "startTimer()" event handler, I added to our pause JS event listener. If the pause button is clicked, the inner text will change to "resume" AND the interval is cleared to our current count. I use the clearInterval() function and pass in our constant "current" that I set equal to our original setInterval() function. By passing in "current" the timer will remain paused at its current number. Now you may see a new function called "setPaused()". I used similar destructuring with useState() to create constants "paused" and "setPaused". The initial state of "paused" is false when the document is loaded. Thus, when we do click the pause button, we now setPaused to equal true.

Then, if the button's inner text equates to "resume" the inner text of course changes back to "pause" and I resume the counter using setInterval() identical to our constant "current". I finally setPaused back to its original state "false".

Our browser should look like this...

First loaded in the browser:
Screen Shot 2021-08-30 at 2.18.04 PM

Start button is clicked + counter is incrementing:
Screen Shot 2021-08-31 at 11.30.13 AM

Pause button is clicked + counter pauses:
Screen Shot 2021-08-31 at 11.30.29 AM

Stop button is clicked + our counter is removed from the page:
Screen Shot 2021-08-31 at 11.30.37 AM

Summary

This is a simple app in concept, but as we can see, it can be complex. Despite its underwhelming browser presence, it touches upon many JS + React fundamentals and deeply portrays the need for a good understanding of these fundamentals. It may not be perfect, but it works! This attitude may take you far in coding, as it has helped me. Start with the basics + fundamentals and they will expand as you learn and code every day. I hope this has helped you as it as helped me.

🌵Comment below for any questions, suggestions + anything else🌵

☁️Let's keep learning + coding together!☁️

Discussion (2)

Collapse
lukeshiru profile image
LUKESHIRU • Edited

One thing you could do to make this more reusable is to put the logic for interval and the timer into custom hooks. So you'll have something like this for interval:

import { useEffect } from "react";

export const interval =
    (delay = 0) =>
    /** @param {() => void} callback */ callback =>
        useEffect(() => {
            const id = setInterval(callback, delay);

            return () => clearInterval(id);
        }, [callback]);
Enter fullscreen mode Exit fullscreen mode

This for the timer:

import { useCallback, useState } from "react";
import { interval } from "./interval";

const use1Second = interval(1e3);

export const useTimer = ({
    seconds: initialSeconds = 0,
    running: initiallyRunning = false
} = {}) => {
    const [seconds, setSeconds] = useState(initialSeconds);
    const [running, setRunning] = useState(initiallyRunning);
    const tick = useCallback(
        () => (running ? setSeconds(seconds => seconds + 1) : undefined),
        [running]
    );
    const start = () => setRunning(true);
    const pause = () => setRunning(false);
    const reset = () => setSeconds(0);
    const stop = () => {
        pause();
        reset();
    };

    use1Second(tick);

    return { pause, reset, running, seconds, start, stop };
};
Enter fullscreen mode Exit fullscreen mode

And finally, use them in your Timer component:

import { useTimer } from "./useTimer";

export const Timer = () => {
    const { seconds, start, stop } = useTimer();

    return (
        <div className="counter-container">
            <button className="start-button" onClick={start}>
                start
            </button>
            <button className="stop-button" onClick={stop}>
                stop
            </button>
            <p id="counter">{seconds}</p>
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

Here's a live example:

Cheers!

Collapse
am20dipi profile image
Adriana DiPietro Author

Wow. Great ideas! You really dried up the code :)