DEV Community 👩‍💻👨‍💻

Amir Ghezala
Amir Ghezala

Posted on

Cool kids handle state with Hooks

A React application is basically a set of React components put together to serve the application’s purpose. These components can be either functional or classes. Functional components are functions that receive props (properties) and return JSX code that is rendered to the screen. They are categorized as stateless components because they do not utilize state and lifecycle methods.

However, prior to 16.8, if you wanted to have a state in your component or wanted to use life-cycle methods, you would need to make your component as a class-based one. Using both types of components has its own advantages when creating an application. However, conversion between them is really annoying and knowing which lifecycle to use, when and how to use it correctly is really challenging when it comes to complex applications.

React 16.8 introduces a new feature: hooks. React hooks are a fundamental change since they make it finally possible to create stateful (with state) function components!

This writeup aims to showcase the current state of state management in React. We will take the example of a simple calculator app and implement it using class components logic, then using two different React Hooks: useState and useReducer. By doing so, we will go through state manipulation in both class and function components.


The final result of our calculator app will look as follow:

calculator-gif

The calculator accepts two input numbers to performs arithmetic operations according to the selected operator.

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
     ....
    };
  }
  ...
  };
  render() {
    return (
      <form>
        <label>
          <div>
            Number 1 : <input type="text" onChange={this.firstNumUpdate} />
          </div>
          <br />
          <div>
            Number 2 : <input type="text" onChange={this.secondNumUpdate} />
          </div>
          <br />
          <div>
            <select onChange={this.operatorUpdate}>
              <option value="+">+</option>
              <option value="-">-</option>
              <option value="*">*</option>
              <option value="/">/</option>
            </select>
            <br />
            <br />
          </div>
          <input type="button" onClick={this.executeComputation} value="Execute" />
          <div />
          <br />
          <input type="text" value={this.state.result} />
        </label>
      </form>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

I- Using a class component

To remind you again, we resorted to the class-based type component to create our App in order to catch the user inputs and update the state values accordingly. The state of our app consisted of the following:

  • firstnumber: the Number 1 user input,
  • secondnumber: the Number 2 user input,
  • operator: the operator the user chooses,
  • result: the final result of computing Number 1 and Number 2 with the operator.
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      firstnumber: 0,
      secondnumber: 0,
      operator: "+",
      result: 0
    };
    this.firstNumUpdate = this.firstNumUpdate.bind(this);
    this.secondNumUpdate = this.secondNumUpdate.bind(this);
    this.operatorUpdate = this.operatorUpdate.bind(this);
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

We also had our onChange and onClick handlers that call this.setState to update the dynamic stored values in this.state:

  • firstNumUpdate: function that updates the state value firstnumber according to the Number 1 user input,
  • secondNumUpdate: function that updates the state value secondnumber according to the Number 2 user input,
  • operatorUpdate: function that updates the state value operator according to the operator user selection.
  • executeComputation: function that computes the result depending on the Number 1, Number 2 and the chosen operator.
firstNumUpdate(evt) {
    this.setState({ firstnumber: Number(evt.target.value) });
  }
  secondNumUpdate(evt) {
    this.setState({ secondnumber: Number(evt.target.value) });
  }
  operatorUpdate(evt) {
    this.setState({ operator: evt.target.value });
  }

  executeComputation = () => {
    let z = null;
    let operator = this.state.operator;
    let firstnumber = this.state.firstnumber;
    let secondnumber = this.state.secondnumber;

    switch (operator) {
      case "+":
        z = firstnumber + secondnumber;
        break;
      case "-":
        z = firstnumber - secondnumber;
        break;
      case "/":
        z = firstnumber / secondnumber;
        break;
      case "*":
        z = firstnumber * secondnumber;
        break;
      default:
        throw new Error();
    }

    this.setState({ ...this.state, result: z });
  };
Enter fullscreen mode Exit fullscreen mode

All in all, our class component's return method looks like this:

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      firstnumber: 0,
      secondnumber: 0,
      operator: "+",
      result: 0
    };
    this.firstNumUpdate = this.firstNumUpdate.bind(this);
    this.secondNumUpdate = this.secondNumUpdate.bind(this);
    this.operatorUpdate = this.operatorUpdate.bind(this);
  }

  firstNumUpdate(evt) {
    this.setState({ firstnumber: Number(evt.target.value) });
  }
  secondNumUpdate(evt) {
    this.setState({ secondnumber: Number(evt.target.value) });
  }
  operatorUpdate(evt) {
    this.setState({ operator: evt.target.value });
  }

  executeComputation = () => {
    let z = null;
    let operator = this.state.operator;
    let firstnumber = this.state.firstnumber;
    let secondnumber = this.state.secondnumber;

    switch (operator) {
      case "+":
        z = firstnumber + secondnumber;
        break;
      case "-":
        z = firstnumber - secondnumber;
        break;
      case "/":
        z = firstnumber / secondnumber;
        break;
      case "*":
        z = firstnumber * secondnumber;
        break;
      default:
        throw new Error();
    }

    this.setState({ ...this.state, result: z });
  };

  render() {
    return (
      <form>
        <label>
          <div>
            Number 1 : <input type="text" onChange={this.firstNumUpdate} />
          </div>
          <br />
          <div>
            Number 2 : <input type="text" onChange={this.secondNumUpdate} />
          </div>
          <br />
          <div>
            <select onChange={this.operatorUpdate}>
              <option value="+">+</option>
              <option value="-">-</option>
              <option value="*">*</option>
              <option value="/">/</option>
            </select>
            <br />
            <br />
          </div>
          <input
            type="button"
            onClick={this.executeComputation}
            value="Execute"
          />
          <div />
          <br />
          <input type="text" value={this.state.result} />
        </label>
      </form>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

That’s it for our class component! You can check out the code here.

Now that we’ve seen how our calculator looks like as a class component, let’s implement it using hooks.


II- Using a functional component

a) Using the useState hook

Let’s now implement the same application using a functional component and the useState hook. We can’t use the this.state or this.setState properties anymore because we wont use a class-based component. However, our functional component with the help of hooks will store and update the state. As mentioned before, hooks are React helper functions to create & manipulate the state of your component.

First let’s import useState from React.

import React, { useState } from "react";
Enter fullscreen mode Exit fullscreen mode

We then write our App class-based component as a functional component using the following syntax:

function App() {
Enter fullscreen mode Exit fullscreen mode

We then call the useState hook function that takes an initial state for the user input and returns an array of two elements:

const initialState = {
  firstnumber: 0,
  secondnumber: 0,
  operator: "+",
  result: 0
};

function App() {
  const [state, setState] = useState(initialState);
  ...
}
Enter fullscreen mode Exit fullscreen mode
  • The first element of the array is the state of the object,
  • The second element is a function that is used to update that state. For the initial state we can pass anything, an empty string, 0, null, an empty array, an empty object, whatever kind of state you want to manage initially.
const [state, setState] = useState(initialState);
Enter fullscreen mode Exit fullscreen mode

In our case we decide to put the initial value as “0” for the first number, second number and result input elements, while the operator select input element takes an initial value of “+”.

We can of course use a single useState call and have all our state (including the numbers, the result, and the operator) in a single object. However, the advisable way is to use multiple calls so you can separate your states, change them independently and focus on one state at a time.

When an event is triggered (e.g. the onChange event of the first number input), we use its corresponding state updater function to perform a state update.

const operatorUpdate = evt => {
  setState({ ...state, operator: evt.target.value });
};

const firstNumUpdate = evt => {
  setState({ ...state, firstnumber: Number(evt.target.value) });
};

const secondNumUpdate = evt => {
  setState({ ...state, secondnumber: Number(evt.target.value) });
};

const executeComputation = () => {
  let z = null;
  let operator = state.operator;
  let firstnumber = state.firstnumber;
  let secondnumber = state.secondnumber;

  switch (operator) {
    default:
      z = firstnumber + secondnumber;
      break;
    case "-":
      z = firstnumber - secondnumber;
      break;
    case "/":
      z = firstnumber / secondnumber;
      break;
    case "*":
      z = firstnumber * secondnumber;
      break;
  }

  setState({ ...state, result: z });
};
Enter fullscreen mode Exit fullscreen mode

Voilà 🎉! Checkout how our calculator App looks like here

b) Using the useReducer hook

useState is not the only hook we can use to manipulate our component state. We will see now another hook, useReducer, which helps achieve the same result with a different syntax. This hook uses a reducer with two arguments: a state and an action and returns a new state of the app. If you’ve ever used Redux state management library, you will find the useReducer hook very familiar to Redux’s reducer.

useReducer takes a function (called a reducer) which has 2 arguments: the state and an action. Depending on the action passed to the reducer, a new state of the app is returned. We’ll come back to the reducer in a bit.

Step1: Configuring the useReducer

We first import the useReducer:

import React, { useReducer } from "react";
Enter fullscreen mode Exit fullscreen mode

We then define the hook like so:

const [state, dispatch] = useReducer(reducer, initialState);
Enter fullscreen mode Exit fullscreen mode

Step2: Defining the logic for the user input

Let’s look at our problem again: we wish to add, subtract, multiply or divide 2 numbers. To do that, the user first inputs the 2 numbers. So let’s take a look at our Number 1 and Number 2 input fields.

Since the complexity of the tree is not that high and the data of the user input doesn’t need to be passed from a component to another embedded one, we could add a useState hook, as we did above, just to manipulate the user input. However, for the sake of the example, let’s use useReducer for everything , that is, for handling the user input and the logic of the calculator.

We define two actions: FIRST_NUM_UPDATE and SECOND_NUM_UPDATE in our reducer, representing the actions to be dispatched or “triggered” when the user inputs Number 1 or Number 2 respectively:

function reducer(state, action) {
  const firstnumber = Number(action.firstnumber);
  const secondnumber = Number(action.secondnumber);

  switch (action.type) {
    // User Input actions
    case "FIRST_NUM_UPDATE":
      return {
        ...state,
        firstnumber: firstnumber
      };
    case "SECOND_NUM_UPDATE":
      return {
        ...state,
        secondnumber: secondnumber
      };
    case "OPERATOR_UPDATE":
      return {
        ...state,
        operator: action.operator
      };
    // Computing actions
    case "ADD":
      return {
        ...state,
        result: firstnumber + secondnumber
      };
    case "SUBTRACT":
      return { ...state, result: firstnumber - secondnumber };
    case "MULTIPLY":
      return { ...state, result: firstnumber * secondnumber };
    case "DIVIDE":
      return { ...state, result: firstnumber / secondnumber };
    default:
      throw new Error();
  }
}
Enter fullscreen mode Exit fullscreen mode

The inputted values for firstnumber or secondnumber are passed through the action parameter. Depending on which number was inputted, FIRST_NUM_UPDATE or SECOND_NUM_UPDATE are then dispatched to return a new state containing the newly inputed values of firstnumber and secondnumber.

Now that our reducer handles these actions, let’s actually trigger them whenever the user changes the inputs for the first and second numbers.

const firstNumUpdate = evt => {
    dispatch({
      type: "FIRST_NUM_UPDATE",
      firstnumber: evt.target.value
    });
  };

const secondNumUpdate = evt => {
    dispatch({
      type: "SECOND_NUM_UPDATE",
      secondnumber: evt.target.value
    });
  };
Enter fullscreen mode Exit fullscreen mode

We know that we want to dispatch them during the onChange of Number 1 and Number 2 input fields. So let’s call firstNumUpdate and secondNumUpdate in the onChange handler for each number input field as such:

<div> Number 1 :
  <input type="text" onChange={evt => firstNumUpdate(evt)} value={state.firstnumber} />
</div>
<br /> 
<div> Number 2 :
  <input type="text" onChange={evt => secondNumUpdate(evt)} value={state.secondnumber} />
</div>
Enter fullscreen mode Exit fullscreen mode

Now we have successfully used our reducer to update the state to whatever the user inputs in the number input fields! Let’s do the same to our operator select element:

  • We define the OPERATOR_UPDATE action to return the selected operator in our reducer function
case "OPERATOR_UPDATE":
      return {
        ...state,
        operator: action.operator
      };
Enter fullscreen mode Exit fullscreen mode
  • We define a helper method operatorUpdate to dispatch the OPERATOR_UPDATE action:
const operatorUpdate = evt => {
    const operator = evt.target.value;
    dispatch({
      type: "OPERATOR_UPDATE",
      operator: operator
    });
  };
Enter fullscreen mode Exit fullscreen mode
  • We call operatorUpdate from our onChange handle in our operator select element:
<select onChange={evt => operatorUpdate(evt)}>
      <option value="+">+</option>
      <option value="-">-</option>
      <option value="*">*</option>
      <option value="/">/</option>
</select>
Enter fullscreen mode Exit fullscreen mode

Cool, now let’s get our hands dirty with the calculator’s logic!

Step3: Defining the logic for the calculator

Our calculator gives the user the ability to add, subtract, multiply or divide two numbers. Just from stating the problem we already have 4 reducer actions!

  • ADD action representing the sum of our numbers
case "ADD":
      return {
        ...state,
        result: Number(action.firstnumber) + Number(action.secondnumber)
      };
Enter fullscreen mode Exit fullscreen mode
  • SUBTRACT action representing the subtraction of our numbers:
case "MULTIPLY":
      return { ...state, result: firstnumber * secondnumber };
Enter fullscreen mode Exit fullscreen mode
  • DIVIDE action representing the division of our numbers:
case "DIVIDE":
      return { ...state, result: firstnumber / secondnumber };
Enter fullscreen mode Exit fullscreen mode

Ultimately, our reducer function looks like this:

function reducer(state, action) {
  const firstnumber = Number(action.firstnumber);
  const secondnumber = Number(action.secondnumber);

  switch (action.type) {
    // User Input actions
    case "FIRST_NUM_UPDATE":
      return {
        ...state,
        firstnumber: firstnumber
      };
    case "SECOND_NUM_UPDATE":
      return {
        ...state,
        secondnumber: secondnumber
      };
    case "OPERATOR_UPDATE":
      return {
        ...state,
        operator: action.operator
      };
    // Computing actions
    case "ADD":
      return {
        ...state,
        result: firstnumber + secondnumber
      };
    case "SUBTRACT":
      return { ...state, result: firstnumber - secondnumber };
    case "MULTIPLY":
      return { ...state, result: firstnumber * secondnumber };
    case "DIVIDE":
      return { ...state, result: firstnumber / secondnumber };
    default:
      throw new Error();
  }
}
Enter fullscreen mode Exit fullscreen mode

We then define our helper method executeComputation to dispatch those actions depending on which operator is used:

const executeComputation = () => {
    const operator = state.operator;

    switch (operator) {
      case "+":
        dispatch({
          type: "ADD",
          firstnumber: state.firstnumber,
          secondnumber: state.secondnumber
        });
        break;

      case "-":
        dispatch({
          type: "SUBTRACT",
          firstnumber: state.firstnumber,
          secondnumber: state.secondnumber
        });
        break;

      case "*":
        dispatch({
          type: "MULTIPLY",
          firstnumber: state.firstnumber,
          secondnumber: state.secondnumber
        });
        break;

      case "/":
        dispatch({
          type: "DIVIDE",
          firstnumber: state.firstnumber,
          secondnumber: state.secondnumber
        });
        break;

      default:
        throw new Error();
    }
  };
Enter fullscreen mode Exit fullscreen mode

Now we just need to display the result simply using state.result:

<input type="text" value={state.result} />
Enter fullscreen mode Exit fullscreen mode

And we’re done 🎉! You can check out what we just did here


Conclusion

According to the React documentation, if you are already using class components, you don’t have to switch to hooks. However, you no longer have to use classes just to have state in your component. The useState and useReducer React hooks provide a nice syntax to achieve the create & manipulate state in a function component.

An important notice concerning the rules of using React hooks.

Hooks are used at the top level of your component, for example; you can’t call the hook function nested in another function , or in an if statement or in a loop. Moreover, they have to be component functions that take props and returns JSX.

Some other cool ones to look at would be:

  • useContext: Accepts a React.createContext context object and returns the current context value for that context.
  • useEffect: Similar to componentDidMount and componentDidUpdate.
  • useCallback: Returns a memoized callback.

To read more about Hooks, checkout the Hooks API.

Did you like this article? Was it helpful? Do you have any suggestions to improve it? If you have any thoughts or comments, we would love to hear them!

Notice: My sister @anssamghezala and I are both learning React and publish articles every month. Follow us on twitter @amir_ghezala and @anssam_ghezala to get in touch! :)

Top comments (2)

Collapse
 
outthislife profile image
Talasan Nicholson

onChange={evt => firstNumUpdate(evt)}

Why not just onChange={firstNumUpdate} ?

Collapse
 
anssamghezala profile image
Anssam Ghezala

You're right that works :) Thanks !

Visualizing Promises and Async/Await 🤯

async await

☝️ Check out this all-time classic DEV post