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:
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>
);
}
}
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);
}
...
}
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 });
};
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>
);
}
}
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";
We then write our App class-based component as a functional component using the following syntax:
function App() {
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);
...
}
- 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);
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 });
};
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";
We then define the hook like so:
const [state, dispatch] = useReducer(reducer, initialState);
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 useuseReducer
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();
}
}
The inputted values for firstnumber or secondnumber are passed through the action parameter. Depending on which number was inputted,
FIRST_NUM_UPDATE
orSECOND_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
});
};
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>
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
};
- We define a helper method
operatorUpdate
to dispatch theOPERATOR_UPDATE
action:
const operatorUpdate = evt => {
const operator = evt.target.value;
dispatch({
type: "OPERATOR_UPDATE",
operator: operator
});
};
- 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>
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)
};
-
SUBTRACT
action representing the subtraction of our numbers:
case "MULTIPLY":
return { ...state, result: firstnumber * secondnumber };
-
DIVIDE
action representing the division of our numbers:
case "DIVIDE":
return { ...state, result: firstnumber / secondnumber };
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();
}
}
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();
}
};
Now we just need to display the result simply using state.result:
<input type="text" value={state.result} />
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 aReact.createContext
context object and returns the current context value for that context. -
useEffect
: Similar tocomponentDidMount
andcomponentDidUpdate
. -
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)
onChange={evt => firstNumUpdate(evt)}
Why not just
onChange={firstNumUpdate}
?You're right that works :) Thanks !