React hooks: work with useState and useReducer effectively

In this article, we will discover two hooks: the basic one, useState, and the advanced one, useReducer; both can do the same thing, which is state management/manipulation, but in a different way for different use cases.
Before we dig into each one, let me quickly recap React hooks.
What are React Hooks?
They are collectively called hooks; they are special functions that add state, lifecycle features, performance optimization, and more to your functional components.
There are 10 built-in functions that have unique names; all begin with a three-letter ‘use’, but with different functionality and use cases. They are completely optional; you don’t have to rewrite your existing class, but you can start using them in the new ones since React is all about incremental adoption.
React Hooks are just functions that take an argument and return value; in addition, you can create your own ones easily as a custom hook.
Before using them, you have to be aware of the rules of using these hooks.
We have to respect these rules; otherwise, we will get confusion and unexpected results, since React relies on the order in which Hooks are called; the order of the Hook calls must be the same on every render.
React won’t natively throw an error or warning to let you know; you have to add a linter plugin that ensures and enforces these rules automatically within your projects.
There are only two rules to follow.
- Only Call Hooks at the Top Level, i.e., not within conditionals, loops, or nested functions.
- Only Call Hooks from React functions, i.e., Functional Components and Custom Hooks.
Now, let’s go quickly through some of them.
Starting with the one you will use the most
- useState: This hook gives us the ability to make functional components have their local state through a very simple API, as you can see in the example below.
- useReducer: This hook gives us an easy way to organize our state management; if you are already familiar with Redux, you will find this easy to understand and work with.
- useEffect: This hook has been made for side effects; it’s a combination of componentDidMount, componentDidUpdate, and componentWillUnmount all in one place.
- useContext: This gives us the ability to pass data, config data in our app, down the component tree without using prop drilling or a third-party library like Redux.
- useCallback: Gives us a nice performance gain by memorizing the given callback and updating it when the given dependencies change.
- useMemo: This hook caches some data, saving some compute time in our app and hopefully making it more responsive in the process.
I encourage you to discover the others in the official documentation.
It’s time to learn more about useState and useReducer and see them in practice.
The useState hook
Let’s consider that we have a functional component that represents our app footer like this:
import React from "react";
const Footer = () => {
return (
<footer className="footer">
<div>
<small>Copyright © 2019</small>
</div>
</footer>
);
};
export default Footer;
As you can see, it’s a very simple component. At some point, we need a local state to handle show/hide some extra information by clicking on a button, and as you know, the functional component is stateless, and we can only have state in class components.
Before React Hooks, we should convert this component to a class component so that we can have a state capability, but now with React Hooks, especially useState, we can have a local state in our functional component.
This hook works super straightforward, easy to think about, and much cleaner than using a complex object with setState.
Let’s hook into our functional component with useState to have a local state and handle show/hide our extra footer information.
import React, { useState } from "react";
const Footer = () => {
const [extraInfo, setExtarInfo] = useState(false);
function handleClick() {
setExtarInfo(!extraInfo);
}
return (
<footer className="footer">
<div>
<small>Copyright © 2019</small>
</div>
<br />
<button onClick={handleClick}>More Info.</button>
{extraInfo && (
<p>
Le Lorem Ipsum est simplement du faux texte employé dans la
composition et la mise en page avant impression.
</p>
)}
</footer>
);
};
export default Footer;
Start by importing useState from React, then call useState with a default value. This will return two values in the form of an array, which we have simplified by deconstructing it into two variables. You can think of these two variables as getters and setters in OOP: use extraInfo to get the current state and setExtraInfo to set a new value. extraInfo is just a variable, while setExtraInfo is a function that receives a new state, either a value or a function that returns a value.
We used extraInfo to show or hide the paragraph according to extraInfo’s current state, and use setExtraInfo to update extraInfo’s current state.
You can name these variables whatever you want, but it’s a convention for the second variable to use ‘set’ as a prefix with camel case.
It’s absolutely possible to use an object with useState instead of multiple useState calls, but the React team recommends doing multiple useState calls since it’s pretty easy and handy to deal with variables than an object.
If you end up having a lot of variables, you can use a custom hook (we will talk about it in a separate article) to put them into one state or use useReducer, which we will discover next.
Note: unlike setState, useState actually replaces the old state with the new one, while setState merges them together.
Now let’s discover the useState’s big brother.
The useReducer hook
This hook gives us an easy way to organize our state management.
But first, a quick definition of what a reducer means in the context of a React app.
A reducer is simply a function, a pure function, that takes the previous state as the first parameter and the action as the second parameter, and returns a new state based on the action passed.
This hook is like useState but with additional features; you can think of this as a superset of useState,
If you are already familiar with Redux, you will find this easy to use.
When we have complex state logic that sets different values according to the action needed, useReducer is available to do this for you with a familiar approach to Redux, and as the doc said: “useReducer also lets you optimize performance for components that trigger deep updates because you can pass dispatch down instead of callbacks.”
Let’s see it in practice.
Consider that we have a functional component for logging in users, which has its local state using the useState hook, and as you can see, we have several useState calls; at this point, we are perfectly adopting the recommendation of the React team to use multiple useState calls.
import React, { useState } from 'react';
import { login } from './utils';
export default function LoginUseState() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isLoading, showLoader] = useState(false);
const [error, setError] = useState('');
const [isLoggedIn, setIsLoggedIn] = useState(false);
const onSubmit = async e => {
e.preventDefault();
setError('');
showLoader(true);
try {
await login({ username, password });
setIsLoggedIn(true);
} catch (error) {
setError('Incorrect username or password!');
showLoader(false);
setUsername('');
setPassword('');
}
};
return (
<div className="App">
<div className="login-container">
{isLoggedIn ? (
<>
<h1>Welcome {username}!</h1>
<button onClick={() => setIsLoggedIn(false)}>Log Out</button>
</>
) : (
<form className="form" onSubmit={onSubmit}>
{error && <p className="error">{error}</p>}
<p>Please Login!</p>
<input
type="text"
placeholder="username"
value={username}
onChange={e => setUsername(e.currentTarget.value)}
/>
<input
type="password"
placeholder="password"
autoComplete="new-password"
value={password}
onChange={e => setPassword(e.currentTarget.value)}
/>
<button className="submit" type="submit" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Log In'}
</button>
</form>
)}
</div>
</div>
);
}
The source of this code is from hswolff.
Have you noticed that our states are scattered over the component, giving us a bad experience trying to figure out the logic, add or change something?
Of course, you have noticed that.
This can be hard to scale and maintain in the future since we have to deal individually with each state.
Yes, it’s explicit; actually, it’s too explicit, which eliminates the big-picture level of the logic. We can’t clearly see what’s going on at first glance; we just have to look and think about it.
Hmm, well, can we group these states since they are related to each other? So we can give an intuitive name to each according to its action, like ‘login’, ‘logout’, ‘success’, ‘failed’, and ‘typing’…?
Yes, we can; this is when useReducer shines to make the state management more intuitive.
Let’s do it now using useReducer.
function LoginUseReducer() {
const [state, dispatch] = useReducer(loginReducer, initialState);
const { username, password, isLoading, error, isLoggedIn } = state;
const onSubmit = async e => {
e.preventDefault();
dispatch({ type: 'login' });
try {
await login({ username, password });
dispatch({ type: 'success' });
} catch (error) {
dispatch({ type: 'error' });
}
};
return (
<div className="App">
<div className="login-container">
{isLoggedIn ? (
<>
<h1>Welcome {username}!</h1>
<button onClick={() => dispatch({ type: 'logOut' })}>
Log Out
</button>
</>
) : (
<form className="form" onSubmit={onSubmit}>
{error && <p className="error">{error}</p>}
<p>Please Login!</p>
<input
type="text"
placeholder="username"
value={username}
onChange={e =>
dispatch({
type: 'field',
fieldName: 'username',
payload: e.currentTarget.value,
})
}
/>
<input
type="password"
placeholder="password"
autoComplete="new-password"
value={password}
onChange={e =>
dispatch({
type: 'field',
fieldName: 'password',
payload: e.currentTarget.value,
})
}
/>
<button className="submit" type="submit" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Log In'}
</button>
</form>
)}
</div>
</div>
);
}
function loginReducer(state, action) {
switch (action.type) {
case 'field': {
return {
...state,
[action.fieldName]: action.payload,
};
}
case 'login': {
return {
...state,
error: '',
isLoading: true,
};
}
case 'success': {
return {
...state,
isLoggedIn: true,
isLoading: false,
};
}
case 'error': {
return {
...state,
error: 'Incorrect username or password!',
isLoggedIn: false,
isLoading: false,
username: '',
password: '',
};
}
case 'logOut': {
return {
...state,
isLoggedIn: false,
};
}
default:
return state;
}
}
const initialState = {
username: '',
password: '',
isLoading: false,
error: '',
isLoggedIn: false,
};
The source of this code is from hswolff.
Look how awesome useReducer is; now our code is getting cleaner, easier to co-locate, and easier to understand.
We separate the state management from the component to make it more readable; for example, if we want to change the default values, we have one place to deal with it, which is initialState. If we want to change something about the state, we can go directly to loginReducer…
If you are not familiar with Redux, let me explain this to you.
By invoking useReducer, we have to provide the reducer as the first argument; it’s just a function that will handle the state according to the action passed to it, and the initial state as a second argument, which doesn’t have to be an object; it can be any data type.
const [state, dispatch] = useReducer(loginReducer, initialState);
useReducer is similar to useState; it gives us two variables: the first holds the current state, and the second, dispatch is a function that sends an action definition to the reducer to produce a new state depending on the type and other properties of the action.
The action definition object can have as many properties as needed; for example, in this case we had to pass, in addition to the type and payload, the fieldName.
dispatch({ type: 'field', fieldName: 'password', payload: e.currentTarget.value });
Like useState, useReducer replaces the previous one with the new state, so be aware of that when using this hook; create a new object containing the previous state ‘…state’ followed by the ‘key: value’ properties that we want to modify.
return { ...state, [action.fieldName]: action.payload };
Remember that: state, dispatch, reducer, initialState are just names; you can name them as you want, but it’s better to stick to the conventions.
Note: your reducer should NEVER make an HTTP request! Keep that function pure and side-effect-free.
So, when should I use one and not the other?
It’s up to you and your needs when to use either useState or useReducer.
Here is my suggestion for you: use useState when dealing with a property state that is unrelated to or dependent on another, and use useReducer when you have multiple properties that you want to update differently based on actions, and also when one property in your state relies on the value of another.
Any time you need state x to update state y, that’s an insta-useReducer use case
— Kent C. Dodds
Conclusion
To recap what we’ve learned, hooks have come to solve some very common problems that everyone who uses React comes across; they encourage the use of functional components to benefit from their simplicity and efficiency.
useState and useReducer provide a powerful approach to managing state in functional components for all cases; they are easy to use and think about.
…
Up to 50 claps per reader · 265 on Medium