Building Counter 2.0 - React Fundamentals #2

Building Counter 2.0 - React Fundamentals #2

In our previous blog, we introduced the concept of React state and explored its importance in building dynamic web applications. Now, we'll take our understanding of state to the next level and incorporate it into our Counter app, creating an upgraded version we'll call Counter 2.0. With the help of the useState hook, we'll be able to keep track of the number of times the user has clicked the button, and dynamically update the display to reflect this count. By the end of this blog, you'll have a solid grasp of how to use state in your own React projects. So let's dive in and start building!

Asynchronous Nature of "useState"

But before, we must address some caveats that we may encounter using state in React. Take a look at the code snippet below and tell what should be the output:

function App() {
  const [count, setCount] = React.useState(0);

  return (
    <>
      <p>
        You've clicked {count} times.
      </p>
      <button
        onClick={() => {
          setCount(count + 1);

          console.log(count)
        }}
      >
        Click me!
      </button>
    </>
  );
}

If your answer is 0, congratulations you are correct! But how? When we create our state variable, we initialize it to 0. Then, when we click the button, we increment it by 1, to 1. So shouldn't it log 1, and not 0?

Here's the catch: state setters aren't immediate.

Once we invoke the setCount function, React receives our request to modify a state variable. React doesn't perform the update right away, but waits until the current operation, such as handling a click event, is finished. Once the operation is completed, React updates the state value and re-renders the component.

It's crucial to understand that updating a state variable is not a synchronous process, but asynchronous instead. The updated value affects the state for the upcoming render cycle, and the update itself is scheduled to take place at a later time.

Here's how we can fix the code:

function App() {
  const [count, setCount] = React.useState(0);

  return (
    <>
      <p>
        You've clicked {count} times.
      </p>
      <button
        onClick={() => {
          const nextCount = count +1
          setCount(nextCount);

          console.log(nextCount)
        }}
      >
        Click me!
      </button>
    </>
  );
}

We store the updated value in a variable nextCount, so that we can access that variable whenever we want to know what the new value is.

Let's build Counter 2.0

You can kickstart a new react project on your machine using node package manager of your choice(yarn/npm/pnpm)

yarn create vite counter2 --template react

or create a react project on Codesanbox like me.

Next, install 'react-feather' package library. It will be used for icons for our counter app

yarn add react-feather

Add in the style.css and reset.css files from my sandbox and let's lay the foundation of our code.

import React from 'react';
import { ChevronUp, ChevronsUp, ChevronDown, ChevronsDown, RotateCcw, Hash } from 'react-feather'

function Counter() {
  const [count, setCount] = React.useState(0);

  return (
    <div className="wrapper">
      <p>
        <span>Current value:</span>
        <span className="count">
          {count}
        </span>
      </p>
      <div className="button-row">
        <button>
          <ChevronUp />
          <span className="visually-hidden">
            Increase slightly
          </span>
        </button>
        <button>
          <ChevronsUp />
          <span className="visually-hidden">
            Increase a lot
          </span>
        </button>
        <button>
          <RotateCcw />
          <span className="visually-hidden">
            Reset
          </span>
        </button>
        <button>
          <Hash />
          <span className="visually-hidden">
            Set to random value
          </span>
        </button>
        <button>
          <ChevronsDown />
          <span className="visually-hidden">
            Decrease a lot
          </span>
        </button>
        <button>
          <ChevronDown />
          <span className="visually-hidden">
            Decrease slightly
          </span>
        </button>
      </div>
    </div>
  );
}

export default Counter;

It will look something like this but will not have any functionality yet and our task is just that. The code is pretty self-explanatory so far and one by one let's add it's due functionality.

"Increase Slightly" Button

The goal here is to increase the counter by just 1 unit when the button is clicked. This will be a very basic implementation and the code of our basic counter(covered in the first part) will be used.

  <button onClick={() => setCount(count+1)}>
          <ChevronUp />
          <span className="visually-hidden">
            Increase slightly
          </span>
  </button>

"Decrease Slightly" Button

The logic here is as of the "Increase Slightly" button but just instead of adding 1 unit to the counter we'll decrease 1 unit.

  <button onClick={() => setCount(count-1)}>
          <ChevronDown />
          <span className="visually-hidden">
            Increase slightly
          </span>
  </button>

"Increase a lot" Button

Here our goal is to increase the counter value by 10 whenever the button is clicked. So the logic is similar to the first two buttons we made

 <button onClick={() => setCount(count +10)}>
          <ChevronsUp />
          <span className="visually-hidden">
            Increase a lot
          </span>
  </button>

"Decrease a lot" Button

Similar to "Increase a lot" button we'll implement this by adding the subtraction logic.

 <button onClick={() => setCount(count -10)}>
          <ChevronsUp />
          <span className="visually-hidden">
            Increase a lot
          </span>
  </button>

Reset Button

Our goal here is to reset the value of the counter to the initial value(whatever we fixed in the start). Now here instead of assigning "0" to our useState hook, we can define a variable where we assign the initial value

function Counter() {
  const initialVal = 0
  const [count, setCount] = React.useState(initialVal);
return (
 ...   
    <button onCLick={() => setCount(intialVal)}>
          <RotateCcw />
          <span className="visually-hidden">
            Reset
          </span>
     </button>

"Set a random number " Button

This is the tricky part of the project and that's why I explained the asynchronous working of state in the beginning because we are going to use it here. To create a random number we can use the inbuilt Math.random() function which combined with Math.ceil() function will provide whole numbers and our goal here is to provide a random number in between 1-100.

<button onClick={() => {
    const nextNum = Math.ceil(Math.random() *100)
    setCount(nextNum)
}}>
          <Hash />
          <span className="visually-hidden">
            Set to random value
          </span>
</button>

There is just one tiny little flaw in this code that is that Math.random() cannot produce number "0" and if it's exactly "0" it won't be rounded off to "1". The odds of this happening are astronomically low but let's just address this case also.

function Counter() {
const clamp = (val, min = 0, max = 1) => {
  if (min > max) {
    [min, max] = [max, min];
  } 
...
return (
...
<button onClick={() => {
    const nextNum = clamp(Math.ceil(Math.random() *100), 1, 100)
    setCount(nextNum)
}}>
          <Hash />
          <span className="visually-hidden">
            Set to random value
          </span>
</button>

To resolve that issue we'll use this clamp function, which uses Math.max() and Math.min() to clamp a value between a given "minimum" and a given "maximum". Then we'll clamp our button values from 1 to 100 and we'll be good to go.

Finally, our Counter app is complete and the code looks like this:

import React from "react";
import {
  ChevronUp,
  ChevronsUp,
  ChevronDown,
  ChevronsDown,
  RotateCcw,
  Hash
} from "react-feather";

const clamp = (val, min = 0, max = 1) => {
  if (min > max) {
    [min, max] = [max, min];
  }

  return Math.max(min, Math.min(max, val));
};

function Counter({ initialVal = 0 }) {
  const [count, setCount] = React.useState(initialVal);

  return (
    <div className="wrapper">
      <p>
        <span>Current value:</span>
        <span className="count">{count}</span>
      </p>
      <div className="button-row">
        <button onClick={() => setCount(count + 1)}>
          <ChevronUp />
          <span className="visually-hidden">Increase slightly</span>
        </button>
        <button onClick={() => setCount(count + 10)}>
          <ChevronsUp />
          <span className="visually-hidden">Increase a lot</span>
        </button>
        <button onClick={() => setCount(initialVal)}>
          <RotateCcw />
          <span className="visually-hidden">Reset</span>
        </button>
        <button
          onClick={() => {
            const nextNum = clamp(Math.ceil(Math.random() * 100),1,100)
            setCount(nextNum);
          }}
        >
          <Hash />
          <span className="visually-hidden">Set to random value</span>
        </button>
        <button onClick={() => setCount(count - 10)}>
          <ChevronsDown />
          <span className="visually-hidden">Decrease a lot</span>
        </button>
        <button onClick={() => setCount(count - 1)}>
          <ChevronDown />
          <span className="visually-hidden">Decrease slightly</span>
        </button>
      </div>
    </div>
  );
}

export default Counter;

Thank you for reading my blog. If you learned something or find this article useful, please like it and share with others. Bye :)!