A guide to the React useState hook
React hooks have been around for quite a while now (since the release of React 16.8) and have changed the way developers go about building React components — by basically writing JavaScript functions that can leverage the best parts of React such as state, context and some form of lifecycle behaviors.
A React component using hooks is defined using the regular JavaScript function syntax, not the ES6 class syntax. Now that makes a lot of sense, considering that prior to React hooks, a React component defined as a function will have to be refactored to use the ES6 class syntax, in order to add say some piece of state or lifecycle methods to the component. With React hooks, that is no longer necessary, as functions can still remain functions while leveraging the good parts of React. And there’s still more.
That said, React ships with a handful of hooks — useState
, useEffect
, useContext
, useRef
, useMemo
, useCallback
, useLayoutEffect
, useReducer
, etc. In this article, we will focus on how to provision state on React components using the useState
hook. You can learn more about React hooks from the React documentation.
Let’s consider an example to help us understand how React hooks empower function components. Let’s say we have this simple Countdown
component that takes an initial
prop.
import React from "react";
// Countdown component
// Render it like so:
// <Countdown initial={60} />
function Countdown({ initial = 10 }) {
initial = +initial || 10;
return `${initial}`;
}
At the moment, the Countdown
component only renders the initial value and does nothing else — making it a very useless countdown component. To bring it to life, we’ll need to provision a piece of state for the current
value of the countdown, as well as a lifecycle method that is fired when the component mounts — to start an interval that decrements the current value by 1 every second.
Prior to React hooks, the most common way to do this will be to refactor the Countdown
component to use ES6 class syntax like so:
import React from "react";
// Prior to React Hooks
class Countdown extends React.Component {
constructor(props) {
super(props);
this.timerId = null;
this.state = { current: +props.initial || 10 };
}
clearTimer() {
clearInterval(this.timerId);
this.timerId = null;
}
componentDidMount() {
this.timerId = setInterval(() => {
const current = this.state.current - 1;
if (current === 0) this.clearTimer();
this.setState({ current });
}, 1000);
}
componentWillUnmount() {
this.clearTimer();
}
render() {
return `${this.state.current}`;
}
}
But with React hooks, we have yet another way to do this — and the component still remains a regular function, just like it was initially.
import React, { useState, useEffect } from "react";
// With React Hooks
function Countdown({ initial = 10 }) {
const [ current, setCurrent ] = useState(+initial || 10);
useEffect(() => {
if (current <= 0) return;
const timerId = setInterval(() => {
setCurrent(current - 1);
}, 1000);
return function clearTimer() {
clearInterval(timerId);
}
}, [current]);
return `${current}`;
}
It is also possible to build your own custom hook from other React hooks. This custom hook is basically a function and can be used in much the same way as any other React hook. The ability to create custom hooks makes it possible for developers to encapsulate logic or behaviors that can be shared across multiple React components.
For example, we can create a custom hook called useCountdown
that encapsulates the countdown state and lifecycle behaviors from our previous example.
import { useState, useEffect } from "react";
// useCountdown
// Custom React hook
// Uses: useState, useEffect
export default function useCountdown(initial) {
const [ current, setCurrent ] = useState(+initial || 10);
useEffect(() => {
if (current <= 0) return;
const timerId = setInterval(() => {
setCurrent(current - 1);
}, 1000);
return function clearTimer() {
clearInterval(timerId);
}
}, [current]);
return current;
}
We can then go ahead and refactor the Countdown
component to use our custom hook like so:
import React from "react";
import useCountdown from "/path/to/useCountdown";
function Countdown({ initial = 10 }) {
const current = useCountdown(initial);
return `${current}`;
}
The previous example looks trivial and fails to really underscore the importance of having a custom hook like useCountdown
. So let’s say we have another component in our app that uses the useCountdown
hook like so:
import React from "react";
import useCountdown from "/path/to/useCountdown";
function SessionCountdown() {
const time = useCountdown(120);
let minutes = Math.floor(time / 60);
let seconds = time % 60;
let datetime = '';
if (minutes > 0) {
datetime += `${minutes}m`;
}
if (seconds > 0 || (seconds === 0 && minutes === 0)) {
datetime += `${seconds}s`;
}
minutes = `00${minutes}`.slice(-2);
seconds = `00${seconds}`.slice(-2);
return time > 0
? <p aria-live="polite">
This session will expire after{' '}
<time role="timer" dateTime={datetime}>{minutes}:{seconds}</time>.
</p>
: <p aria-live="assertive">The session has expired.</p>
}
From this example, you can see how reusable the countdown logic has become, having been encapsulated into the useCountdown
custom hook.
Now that we’ve had our run through this very short introduction to React hooks, let’s move on to learn about the useState
hook in even more detail.
Provisioning State
One of the most commonly used React feature is state — the ability of a React component to encapsulate one or more pieces of data that determine how it should render or behave, and that can change during its lifecycle.
If you have written React for a while, you might already be familiar with how to setup state in a class component using this.state
to declare and access the state, and this.setState
to update the state.
In case that is not familiar to you, consider this RandomCodeGenerator
component. I have made efforts to highlight the following:
- Declaring the state
- Accessing / Reading the state
- Updating the state
import React from "react";
class RandomCodeGenerator extends React.Component {
constructor(props) {
super(props);
// 1. Declare the component state — with only 1 state variable (`code`)
this.state = {
code: this.generateCode()
};
this.handleButtonClick = this.handleButtonClick.bind(this);
}
generateCode() {
// Returns a random 6-digit code
return String(Math.random() * 1e3).split('.')[1].slice(0, 6);
}
handleButtonClick() {
// 3. Update the component state
this.setState({ code: this.generateCode() });
}
render() {
return <div>
{/* 2. Access the component state */}
<pre>{ this.state.code }</pre>
<button type="button" onClick={this.handleButtonClick}>New Code</button>
</div>
}
}
If this RandomCodeGenerator
component were to be rewritten as a function component, we immediately recognize one problem — what this
refers to? Or simply put, we have no this
. Hence, trying to declare state or access it using this.state
isn’t feasible. In the same vein, we won’t be able to update the state using this.setState
.
The above challenge underscores why we need the useState
hook. The useState
hook allows us declare one or more state variables in function components. Under the hood, React keeps track of these state variables and ensures they stay up-to-date on subsequent re-renders of the component (trust me when I say this — that is some JavaScript closures magic happening there).
That said, let’s go ahead and refactor the RandomCodeGenerator
component to a function component and bring the useState
hook into the mix.
import React, { useState } from "react";
function RandomCodeGenerator() {
// 1. Declare the component state — with only 1 state variable (`code`)
const [ code, setCode ] = useState(generateCode());
const handleButtonClick = () => {
// 3. Update the component state (`code`) — by calling `setCode`
setCode(generateCode());
}
return <div>
{/* 2. Access the component state (`code`) */}
<pre>{code}</pre>
<button type="button" onClick={handleButtonClick}>New Code</button>
</div>
}
// This function has been moved out of the component
// to ensure only one copy of it exists.
function generateCode() {
// Returns a random 6-digit code
return String(Math.random() * 1e3).split('.')[1].slice(0, 6);
}
If you pay close attention to the hooks version of the RandomCodeGenerator
component, you’ll notice that we don’t have any reference to this
. You’ll also notice that the state variable we declared using the useState
hook (code
in this case) had it’s own state update function (we called it setCode
by convention), which you could liken to this.setState
from before — however, they have striking differences as we will see in a bit.
Now we have seen how to provision state on React components, let’s go ahead to examine the useState
hook, its call signature and its return value.
Anatomy of the useState hook
Like every other React hook, the useState
hook is a special JavaScript function and hence must be invoked as a function. When it is invoked inside a function component, it declares a piece of state which React keeps track of under the hood for subsequent re-renders of the component.
Accessing the useState hook
The useState
hook is a standard hook that ships with React and it is declared as a named export of the React module. Hence, you can access it in the following ways:
Directly from the React
default export (React.useState
)
import React from "react";
function SomeComponent() {
const [state, setState] = React.useState();
// ...the rest happens here
}
As a named export of the React module (useState
)
import React, { useState } from "react";
function SomeComponent() {
const [state, setState] = useState();
// ...the rest happens here
}
Call Signature of the useState hook
Having established that the useState
hook is a function, it is important to understand its call signature — what arguments it should be invoked with. The arity of the useState
hook is 1
— which by definition means that it should be called with at least 0 or 1 arguments.
The first argument passed to useState
maps to the initial value of the declared state when the component is first rendered (mounted), and can be any valid JavaScript value. However, if it is called without arguments, then it behaves as though it was called with undefined
as the initial state value (this is not something special — it is standard JavaScript behavior for missing parameter values).
Hence, the following calls to useState
behave in the same way (the initial state value will be undefined
):
// These two calls to `useState` are equivalent
// as it regards their initial state value — `undefined`
useState();
useState(undefined);
Since the initial state value passed to useState
can be any valid JavaScript value, it is possible to pass a function as the initial state value. If that’s the case, React first invokes the function and then uses its return value as the initial state value. This can be very useful if computing the intial state value might be quite expensive. We will look more into this in the next section.
import React, { useState } from "react";
function SomeComponent(props) {
const [state, setState] = useState(() => {
// ...some expensive computation happens here
const initialState = someExpensiveComputation(props);
return initialState;
});
// ...the rest happens here
}
Return value of the useState hook
It is possible that you might have been wondering why we have the following square brackets ([...]
) syntax (on the LHS of the assignment operation) whenever we declare a state variable using the useState
hook:
const [state, setState] = useState(0);
That is because the useState
hook returns an array containing a pair of values whenever it is called. Hence, the reason why we are using the array destructuring syntax to get the values from the array, and assign them to local variables within the function. You can learn more about the destructuring syntax from this article.
Every call to the useState
hook returns an array containing two values:
-
The first value is the current (up-to-date) value of the declared piece of state. React keeps track of this value as it changes and ensures that it stays up-to-date on subsequent re-renders of the component. On the first render (mount) of the component, this value will be the same as the initial state value that was passed to the
useState
hook. -
The second value is the state update function for the declared piece of state. React ensures that this function is the same across the lifecycle of the component. This function, when invoked, updates the value of the corresponding piece of state. If the updated value is not the same as the current value based on
Object.is
comparison, then React schedules a re-render of the component. We will learn more about the state update function shortly.
Now that we understand the nature of the value returned from the useState
hook, you can take a brief moment to look at the code snippets from before, particularly the ones where we had to declare state variables using the useState
hook, and see if you appreciate them better now.
Initial State
We have been able to establish that the useState
hook can be called with any valid JavaScript value, which represents the initial state value of the declared state. Now, let’s put on our JavaScript thinking caps and examine something closely.
For class components, it is understandable that the constructor
function is more like an initialization function, because it gets called only once in the component’s lifecycle — that is, as part of the component’s mount operations (before the first render). That makes sense, since we would like to do things like declaring state and other instance properties in the component’s constructor
function. Then of course, lifecycle methods get fired in their proper sequence, and then the render
method is called to render the component, and finally the componentDidMount
lifecycle method.
For function components, it is not the same. Each time the component is to be rendered (whether during initial mount or after an update), the function will be invoked again, and of course the return value of the function will be rendered. This means that one invocation of the function has entirely no connection with the other invocation.
So putting hooks in perspective, what does this mean for the useState
hook? You might expect that the state variables will be declared afresh each time the function is called, say during a re-render — which means that all the state variables that have been declared in the function component will be reset to their initial state value on each re-render. We already know that’s not the case — React does one better.
Under the hood, React knows when the function is invoked the first time in order to mount the component. During that first invocation, React sets up all the declared state variables — using the initial state values that have been specified and then provides a mechanism to keep track of their changes.
When the function is invoked again on a subsequent re-render, React knows it has already setup state variables for the component. Hence, the call to useState
on a subsequent re-render does pretty much nothing, other than returning the expected array containing the current up-to-date value of the declared state variable as well as the corresponding state update function.
Now here is what we’ve been trying to establish:
The initial state value passed to useState
is only useful the first time the component renders. On subsequent re-renders, React simply ignores or disregards it. This also means that if a function is passed as the initial state value, then the function is only invoked during the first render of the component.
Lazy Initial State
Do you remember our RandomCodeGenerator
component from before? Here is a piece of it:
import React, { useState } from "react";
function RandomCodeGenerator() {
// 1. Declare the component state — with only 1 state variable (`code`)
const [ code, setCode ] = useState(generateCode());
// ...other stuffs happen here
}
If you notice closely, you’ll see that we are declaring a state variable using the useState
hook and that we are passing the return value from the generateCode()
function invocation as the initial state value. Prior to this time, we saw that we could pass the function directly to the useState
hook, knowing that it will be executed at the point of setting up the state variable like so:
import React, { useState } from "react";
function RandomCodeGenerator() {
// 1. Declare the component state
// Notice we are no longer invoking the `generateCode` function,
// rather we are passing it directly to `useState`.
const [ code, setCode ] = useState(generateCode);
// ...other stuffs happen here
}
Both approaches yield the same result, however, there are striking differences.
-
In the former, we are invoking the
generateCode
function and passing its return value to theuseState
hook as the initial state value. In the latter, the invocation of thegenerateCode
function is deferred until React is ready to setup the state variable. This is referred to as lazy initial state. -
In the former, the invocation of the
generateCode
function will always happen for every render, even though React is only interested in its return value during the first render. This means, in the case that the operations performed by thegenerateCode
function are quite expensive, those expensive operations will be performed for every re-render, even though they are redundant. However, in the latter, the invocation of thegenerateCode
function will only happen for the first render, and skipped for subsequent re-renders. That makes a lot of sense, if invoking the function is expensive.
Having made the above points, I believe the RandomCodeGenerator
component from before needs an update. Here is the updated version of it (see if you can spot the changes):
import React, { useState } from "react";
function RandomCodeGenerator() {
// 1. Declare the component state — with only 1 state variable (`code`)
const [ code, setCode ] = useState(generateCode);
const handleButtonClick = () => {
// 3. Update the component state (`code`) — by calling `setCode`
setCode(generateCode());
}
return <div>
{/* 2. Access the component state (`code`) */}
<pre>{code}</pre>
<button type="button" onClick={handleButtonClick}>New Code</button>
</div>
}
// This function has been moved out of the component
// to ensure only one copy of it exists.
function generateCode() {
// Returns a random 6-digit code
return String(Math.random() * 1e3).split('.')[1].slice(0, 6);
}
State Update Function
Earlier, we talked about the state update function, being the second value in the array that gets returned when useState
is called. For the remaining parts of this section, let’s refer to the state update function as setState
— of course it can be named anything, but let’s just stick to convention here.
The arity of the setState
function is 1
. It must be called with at least one argument. The argument passed to the setState
function is the value the corresponding state variable should be updated with, and can be any valid JavaScript value.
Here is an example using a ClickCountButton
component.
import React, { useState } from "react";
function ClickCountButton() {
const [ count, setCount ] = useState(0);
const handleClick = () => {
// Call `setCount` with the incremented `count` value.
setCount(count + 1);
}
return <button type="button" onClick={handleClick}>
I have been clicked {count} {count === 1 ? ' time' : ' times'}.
</button>
}
If a function, however, is passed to the setState
function, then React invokes the function passing the current state value as argument, and then takes the return value of the function invocation as the updated state value. This makes a lot of sense for cases where the next (updated) state can be derived from the previous state.
To explain this further, let’s consider a ToggleSwitch
component with a piece of state called turnedOn
that indicates whether the switch is turned on or not. For this component, turnedOn
can only have two possible values — true
or false
. Let’s say we decide to update (toggle) the turnedOn
state value whenever the switch is clicked. That means that if the current value of the state is true
then the next state value should be false
(which is equivalent to !true
).
Hence, the relationship between the previous state and the next state looks like this:
// If `prevState` = `true`, `nextState` = `false`
// If `prevState` = `false`, `nextState` = `true`
nextState = !prevState;
Bringing everything together, the state update for the turnedOn
state should look like this (assuming the state update function is named as setTurnedOn
):
// The next state can be derived by toggling the previous state
// using the logical NOT (!) operator.
setTurnedOn(prevState => !prevState);
Here’s the complete ToggleSwitch
component.
import React, { useState } from "react";
function ToggleSwitch({ on }) {
const [ turnedOn, setTurnedOn ] = useState(() => Boolean(on) === true);
const handleClick = () => {
setTurnedOn(prevState => !prevState);
};
return <button type="button" role="switch" aria-checked={turnedOn} onClick={handleClick}>
{ turnedOn ? 'Turn OFF' : 'Turn ON' }
</button>
}
Replacing State
In one of the earlier sections, I mentioned that the state update function can be likened to this.setState
in a class component — however, they have a striking difference.
The state update function replaces (overwrites) the current state value with the new value, whereas this.setState
merges the new state value with the current state value (patches). Let’s say we declared our state with an object as value like so:
const [ state, setState ] = useState({
age: 16,
name: 'Glad Chinda',
country: 'Nigeria'
});
Let’s say we want to update the value of the age
property in the state object. Using this.setState
in a class component, I could do this:
// This patches the state object
// and assigns the updated value to the `age` property
// {
// age: 26,
// name: 'Glad Chinda',
// country: 'Nigeria'
// }
this.setState({ age: 26 });
If we do the same using the state update function, we will end up replacing the state with the new state value. Hence, the new state value will be ({ age: 26 }
) — we will no longer have the name
and country
properties in the new state object.
The correct way to update only the age
property of the state object using the state update function will be to use Object.assign
or the object spread operator to extend the current state object and then patch it with the new value like so:
// Using Object.assign()
setState(Object.assign(state, { age: 26 }));
// Or, alternatively
// Using the object spread operator
setState({ ...state, age: 26 });
Whenever a state variable is updated with a value that is the same as its current state value (based on the Object.is
comparison), it basically implies that there wasn’t a state change — hence, React bails out of the state update without rendering the children or firing effects.
State Updates in Unmounted Components
Imagine the state update function is called as part of an async operation — say, after fetching some data from a backend service. It is possible that before the state update function gets called, the component might have unmounted already, maybe due to the user interacting with the UI or something else. The state update function still gets called regardless, even though there is no component to update.
In cases like this, React always shows a warning (in development mode), informing the developer of trying to update the state of a component that has already been unmounted.
To avoid the above scenario, it becomes necessary to keep track of the mountedness of the component. If the component is still mounted, then it is okay to update its state. Otherwise, the state update is ignored.
For ES6 class components, we can set a mounted
instance property to true
inside the componentDidMount
lifecycle method, and set the value of the property to false
when the component unmounts (inside the componentWillUnmount
lifecycle method). Then whenever we need to update state, we first check if the value of the mounted
property is true
before proceeding with the state update.
import React from "react";
class SomeComponent extends React.Component {
componentDidMount() {
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
}
stateChangingMethod() {
if (this.mounted) {
this.setState(/* ...state update... */);
}
}
}
For function components using React hooks, setting up a property such as mounted
, that can be persisted through out the lifecycle of the component, is possible using the useRef
hook.
Here is an updated version of the ToggleSwitch
component from before, that avoids unwanted attempts to update state when the component has unmounted.
import React, { useState, useEffect, useRef } from "react";
function ToggleSwitch({ on }) {
const [ turnedOn, setTurnedOn ] = useState(() => Boolean(on) === true);
// Declare a persistent `mounted` variable
// Initialize `mounted` to `true`
const mounted = useRef(true);
const handleClick = () => {
// Only update the state if `mounted` is `true`
if (mounted.current === true) {
setTurnedOn(prevState => !prevState);
}
};
useEffect(() => {
return () => {
// Set `mounted` to `false` when unmounted
mounted.current = false;
};
}, [mounted]);
return <button role="switch" aria-checked={turnedOn} onClick={handleClick}>
{ turnedOn ? 'Turn OFF' : 'Turn ON' }
</button>
}
We can wrap this logic in a custom hook called useMounted
, that will be reusable across multiple React components.
import { useEffect, useRef } from 'react';
export default function useMounted() {
const mounted = useRef(true);
useEffect(() => () => {
mounted.current = false;
}, [mounted]);
return mounted;
}
The ToggleSwitch
component can then be re-written like so:
import React, { useState } from "react";
import useMounted from "/path/to/useMounted";
function ToggleSwitch({ on }) {
const mounted = useMounted();
const [ turnedOn, setTurnedOn ] = useState(() => Boolean(on) === true);
const handleClick = () => {
if (mounted.current === true) {
setTurnedOn(prevState => !prevState);
}
};
return <button role="switch" aria-checked={turnedOn} onClick={handleClick}>
{ turnedOn ? 'Turn OFF' : 'Turn ON' }
</button>
}
Multiple Pieces of State
Most of the components (if not all) we’ve considered so far, declare only one piece of state. In reality, there will be React components that require multiple pieces of state. For class components, provisioning multiple pieces of state is pretty straightforward, since this.state
is usually declared as a JavaScript object — and each piece of state is represented by a key in the state object.
Multiple useState Declarations
For function components using React hooks, we can declare as many pieces of state as we need using multiple useState
calls. Each piece of state stands on its own, and has its corresponding state update function.
import React, { useState } from "react";
function UserProfile(props) {
// Declaring multiple pieces of state
const [ age, setAge ] = useState(props.age);
const [ name, setName ] = useState(props.fullname);
const [ country, setCountry ] = useState(props.country);
// ...other stuffs happen here
}
Using Plain JavaScript Object
Alternatively, we can decide to declare the state as a JavaScript object — however, we must keep in mind that the state update function will replace the state value as opposed to merging the states.
import React, { useState } from "react";
function UserProfile(props) {
// Declaring multiple pieces of state as an object
const [ profile, setProfile ] = useState({
age: props.age,
name: props.fullname,
country: props.country
});
// ...other stuffs happen here
}
To handle the amount of work that will be required to setup and update multiple pieces of state in a JavaScript object, I have taken the liberty to create a custom hook called useObjectState
(for the purpose of this article). Here is what the custom hook looks like:
import { useState, useEffect, useRef, useCallback } from 'react';
const OBJECT_TYPE = '[object Object]';
const $type = Function.prototype.call.bind(Object.prototype.toString);
const $has = Function.prototype.call.bind(Object.prototype.hasOwnProperty);
export default function useObjectState(objectState) {
const mounted = useRef(true);
const [state, setState] = useState(() => {
if (typeof objectState === 'function') {
objectState = objectState();
}
objectState =
$type(objectState) === OBJECT_TYPE
? Object.assign(Object.create(null), objectState)
: Object.create(null);
return objectState;
});
const setStateMerge = useCallback(
(partialState) => {
if (typeof partialState === 'function') {
partialState = partialState(state);
}
if ($type(partialState) === OBJECT_TYPE) {
partialState = Object.keys(partialState).reduce(
(partialObjectState, key) => {
const value = partialState[key];
return $has(state, key) && !Object.is(state[key], value)
? Object.assign(partialObjectState, { [key]: value })
: partialObjectState;
},
Object.create(null)
);
if (Object.keys(partialState).length > 0 && mounted.current === true) {
setState(Object.assign(Object.create(null), state, partialState));
}
}
},
[state, mounted]
);
useEffect(() => () => {
mounted.current = false;
}, [mounted]);
return [state, setStateMerge];
}
Using the useObjectState
custom hook, the state update function now behaves in a similar fashion as this.setState
, in the sense that it merges state updates. Again, some logic has been included to avoid unnecessary state updates when the component is unmounted.
There is one very important distinction between the state update function produced by useState
and the one produced by the useObjectState
custom hook. While React guarantees that the state update function produced by the useState
hook is the same (stable) across the lifecycle of the component, the one produced by the useObjectState
hook is not guaranteed to always be the same, since it is updated whenever the state object is updated.
This distinction is very important, especially when using React hooks that make provision for declaring dependencies such as useEffect
, useMemo
, useCallback
, etc. If the state update function produced by useObjectState
is used anywhere in the callback function passed to any of the aforementioned hooks, then it must be added to the array of dependencies, whereas the state update function returned by useState
isn’t required to be added as a dependency, since it doesn’t change throughout the lifecycle of the component.
The following code snippet (the AgeGame
component) captures all the concepts that have been described so far (you can take your time to examine it):
import React, { useState, useEffect, useCallback } from "react";
import useObjectState from "/path/to/useObjectState";
function randomAge() {
return Math.floor(Math.random() * 5) + Math.ceil(Math.random() * 56);
}
function AgeGame(props) {
const [ start, setStart ] = useState(() => Date.now());
const [ correct, setCorrect ] = useState(false);
// Declaring multiple pieces of state as an object
const [ profile, setProfile ] = useObjectState(() => ({
age: randomAge(),
name: 'Glad Chinda',
country: 'Nigeria'
}));
// `setProfile` is added to the list of dependencies
const handleClick = useCallback(() => {
setCorrect(false);
setStart(Date.now());
setProfile({ age: randomAge() });
}, [setProfile]);
// The dependencies list hae been omitted intentionally
// Hence, this effect runs for every render
useEffect(() => {
if (profile.age === 26) {
if (correct !== true) setCorrect(true);
return;
}
const timer = setTimeout(() => {
let age;
do {
age = randomAge();
} while (profile.age === age);
setProfile({ age });
}, 250);
return function clearTimer() {
clearTimeout(timer);
};
});
return (
<div>
<p>
<strong>{profile.name}</strong> ({profile.country}) is{' '}
<strong>{profile.age}</strong> years old.
</p>
<p>
{correct
? `✅ Yay!! You got the correct age after ${Math.round(
(Date.now() - start) / 1000
)} seconds.`
: '❌ Oops!! Wrong age'}
</p>
{correct && (
<button type="button" onClick={handleClick}>
Start Over
</button>
)}
</div>
);
}
The useReducer hook
If you don’t mind me asking, Have you ever used Redux — the framework-agnostic state management library? Well, if you have, then you already have an idea of what a reducer is. If you are familiar with the Redux architecture, then you know that a reducer is basically a function that receives a state and an action object, and produces (returns) the next state from the previous state based on the action type and parameters.
It is interesting to know that React ships with a standard useReducer
hook that makes this Redux behavior available to function components, in order to handle more complex state logic involving multiple sub-values (multiple state).
Since this article centers on the useState
hook, I have decided not to go further into how the useReducer
hook works. But for now, just know that it is possible and preferable to manage multiple pieces of state (complex state) in a function component using the useReducer
hook as opposed to using the useState
hook.
You can learn more about the useReducer
hook from the React documentation. In case you are interested in understanding the Redux architecture, the Redux documentation covers it all.
Conclusion
In this article, we’ve been able to understand how React hooks can enable us write function components to leverage React’s awesomeness such as state, context, lifecycle methods, etc. We’ve also been able to learn about the React useState
hook in detail — with emphasis on its call signature, the nature of its return value and how state update functions work.
This article should suffice as a must-have guide for your React hooks toolbox. Having followed through with this article, I will recommend that you go further to learn about the other React hooks that you need to build powerful and modern React applications. For more clarity on areas that were not strongly covered in this article, the React documentation on hooks is there to guide you.
I am glad that you made it to the end of this article — indeed it was a long stretch and I strongly hope it was worth your time. Please remember to leave your feedback in the form of comments and questions or even recommendations or sharing of this article. Also, please don’t hesitate to reach out to me (@gladchinda) if you’d love to see more of this kind of article.
Open Source Session Replay
OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.
Start enjoying your debugging experience - start using OpenReplay for free.