Back

A guide to the React useState hook

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:

  1. Declaring the state
  2. Accessing / Reading the state
  3. 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:

  1. 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.

  2. 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 the useState hook as the initial state value. In the latter, the invocation of the generateCode 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 the generateCode 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 the generateCode 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.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.