A Comprehensive Introduction To React Hooks

Simplify your code while improving extensibility and performance.
Matt Matt (3)
5 minutes
Posted in these interests:
React
h/react3 guides
Howchoo News
h/news143 guides
h/webdev60 guides

React hooks were introduced in React v16.8. Their most basic purpose is to give functions the ability to manage (and share) state, effects and much more while simultaneously simplifying expected behavior, increasing the potential for extensibility and improving performance.

This is meant to be a basic introduction to React hooks, so if all that jargon seems intimidating, don't worry, we'll go step by step into what it all means and how hooks can be useful to you.

Hopefully, this article can give you some insight into the power of React hooks, the methodology behind them, and at least a starting point to begin implementing them in your own projects.

React hooks reduce the number of components in the tree, significantly, making it easier to get a handle on the layout of the component tree and debug without having to wade through tons of Higher Order Components.

Hooks also continue with the general pattern that React has been edging towards for a while in that hooks allow, and encourage, the use of "vanilla" JavaScript by taking (more) advantage of closures and they allow more avenues to implement native JavaScript instead of adding more framework-specific abstractions.

No. As the React team states:

The React team has no current plans to deprecate classes.

"The React team has no current plans to deprecate classes or demand that you refactor all your current class components. They encourage a managed adoption strategy over time and hooks are completely backward compatible with existing React components, though there is the potential for some third-party libraries to have issues that arise as the adoption of hooks becomes more prevalent."

The useState hook is similar to its counterpart with classes this.setState but, in general, is meant for more granular state management and can contain any type, for managing larger state objects, arrays, etc., useReducer is a better-suited hook.

The most common pattern used is array destructuring but if you are not familiar with that pattern, you can always just use the first two elements in the array.

The naming of both the state holder and the state changer are entirely up to you, but just as the convention with hooks is to start them with use, the convention for the state setter is to start it with set.

// State initialized with array destructuring
const [count, setCount] = useState(0);

// State initialized with regular array assignment
const countState = useState(0);
const count = countState[0];
const setCount = countState[1];

Here is a basic example that updates and displays a counter:

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>The current count is {count}</p>
      <button onClick={() => setCount(count + 1)}>}>Increment Counter</button>
    </div>
  );
}

Resources

This is another hook that you will see very often. It deals with side effects and runs after every completed render, but it is versatile enough to be used in place of componentDidMount, componentDidUpdate, componentWillUnmount, and more.

Aside from giving you the opportunity to manage any side effects, there are a few other features that are important to notice with different implementations of useEffect.

useEffect can emulate the functionality of componentWillUnmount by simply returning a function that cleans up after itself. In this returned function, you can unsubscribe from events that were subscribed to inside useEffect or unreferenced anything that is no longer required. Like some of the other hooks, useEffect can have a dependency array as the last parameter where the props, state or other kinds of values can be placed. This prevents unnecessary re-renders, as useEffect will only run if one of its dependent values changes.

Since there is already a custom example of using useEffect for the custom hook I created below, I will use the example from the React Docs below:

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  }, [props.friend.id]); // Only re-subscribe if props.friend.id changes

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

As you can see, the return value of useEffect here is a function used to clean up anything that is not wanted or needed after this component is unmounted. The other thing to notice is the dependency array containing props.friend.id because we only want this useEffect to run if that value changes.

Resources

The useContext hook is just another way to access the context previously defined except it can now be used in a functional component. This still requires a Provider of this specific context type to be somewhere above the component that useContext is called in up the component tree.

You need to use the actual MyContext object initialized with React.createContext to as the parameter in useContext—not MyContext.Provider or MyContext.Consumer.

Using useContext

//...
function functionThatUsesContext() {
  const value = useContext(MyContext);
  // read or subscribe to any of the values of MyContext object
}

Since the large majority of using Context is out of the scope of this hook, I have linked to everything related to using Context from the React Docs below (as it is a topic for a full article itself).

Resources

While useState is an effective way to manage one or more simple state structures, useReducer is designed to handle larger state structures that can be multiple levels deep.

You may be familiar with the concept of a reducer if you have worked with Redux. There are even some great articles on recreating a single Redux type store using useReducer and various combinations of other hooks which I will link to below.

The example in the React Docs is a great basic introduction so I will use that as our basis:

Basic initialization

const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducer example

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}

Just like Redux, the actions contained within the reducer are dispatched by name with the dispatch function and the reducer uses that information to perform an action on the state to create the new state. It can, of course, get more complicated the deeper your state structure goes you will need to remember to return the previous state along with the changes. This is usually done with Object Spread Syntax.

This example below shows how the someOtherStateVar would not be changed when updating only the count:

const initialState = {count: 0, someOtherStateVar: [1,2,3]};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {
        ...state, // spreads the entire Object state into this object
        count: state.count + 1 // overrides the count part of the previous state
      };
    case 'decrement':
      return {
        ...state,
        count: state.count + 1
      };
    default:
      throw new Error();
  }
}

Resources

The details on useCallback in the React Docs is sparse, so here I'll just give some of the cliffnotes on the fantastic article written by Jan Hesters to clarify when to use useCallback vs useMemo and highly encourage you to read that article.

"useCallback gives you referential equality between renders for functions. And useMemo gives you referential equality between renders for values." - Jan Hesters

useCallback is for the cases when you need to call the callback function later, whereas useMemo computes the value that can be referenced later. Both useCallback and useMemo only change when their dependency arrays change.

Basic useCallback example

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

Resources

Simply put, useMemo is used to memoize expensive operations so they are not repeated unless a dependency changes. The last parameter in useMemo, like many other hooks, is the dependency array and this memoizedValue will only be re-calculated if something changes in the dependency array.

Below is a quote from the React team on how to consider the use of useMemo:

"You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance."

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

Resources

useRef creates a mutable JavaScript object that is useful and unique because it keeps the same reference to an object through multiple renders. The way to use useRef is to mutate its { current: ... } property. This will become more clear in the examples below.

Refs have been a concept in React for a while, but their main usage has been to refer to a specific DOM node in order to make mutations to that DOM node with any available functions on that node. This is still basically the case when using the useRef hook, as it is possible to mutate DOM nodes, but useRef has even more capabilities because it can also reference any value that needs to be accessed through any re-renders for the lifetime of the component. This makes it similar to the instance values that are commonly used in classes.

Initializing useRef

import React, { useRef } from 'react';

function someFunction() {
  // ...
  const refContainer = useRef(initialValue);
  // ...
}

An important note to keep in mind when using useRef is that mutating the refContainer.current property does not cause a re-render. If you want to refer to a DOM node and make changes that cause a re-render, useCallback is a better option.

Using useRef to mutate a DOM node

//...
function TextInputWithBlurButton() {
  const inputRef = useRef(null);
  const blurOnButtonClick = () => {
    // `current` points to the mounted text input element
    inputRef.current.blur();
  };

  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={blurOnButtonClick}>Click to blur input</button>
    </>
  );
}

Resources

useLayoutEffect is essentially the same as useEffect and has the same API. The main difference is that it runs synchronously unlike the asynchronous useEffect so it is almost always recommended to use useEffect to prevent unnecessary blocking of the browser updating the screen.

Some common uses for useLayoutEffect are when you need to measure the DOM (screen position, scroll position, etc.) or mutate the DOM causing a synchronous re-render.

Resources

useDebugValue(value);

The main purpose of this hook, as its name suggests, is to help with debugging. It works with React Developer Tools to show the value of the hook that is useful for you to understand the state of the Component that your hook is managing.

useDebugValue(value, [optional formatting function]);

There is also an optional second parameter that is only called when inspecting the value in the developer tools. A good example from the React Documentation is formatting a date into a string on inspection in the developer tools.

useDebugValue(date, () => date.toDateString()); // The formatting function here is only called on dev tools inspection

This is more efficient than performing that same transformation outside of the useDebugValue() hook because the toDateString() function would be called every time the parent hook was called, instead of only when it's looked at in the React Dev Tools.

const formattedDate = date.toDateString(); // This is called on every hook run
useDebugValue(formattedDate);

Resources

Custom React hook countdown clock example
Custom React hook countdown clock example

All together now! Below I am going to explain a custom hook created to use setInterval to aid in the creation of a very basic countdown clock.

Full disclosure, when I initially choose to do a countdown clock using setInterval I found it somewhat difficult to work with. After some searching, the final pieces of this custom hook useInterval were gleaned from Dan Abramov's blog post specifically on using setInterval in hooks and I encourage you to check it out for a more in-depth discussion of why setInterval is particularly tricky to make declarative with React hooks.

If you would like to go straight to the working code, you can see it here on Code Sandbox.

Here is the actual custom hook useInterval created for this:

import React, { useEffect, useState, useRef } from "react";

function useInterval(callback, delay) {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  });

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

Countdown component that makes use of the useInterval custom hook along with useRef to keep the same reference to the initial date and useState to hold both the countdownDuration and the isRunning state that the button uses to pause and un-pause the countdown.

Using the custom useInterval hook

function Countdown() {
  const initDateTime = useRef(Date.now());
  const [countdownDuration, setCountdownDuration] = useState(
    initDateTime.current + 123456789 // Random number choosen to have as a timer duration
  );
  const [isRunning, setIsRunning] = useState(true);

  useInterval(
    () => {
      setCountdownDuration(countdownDuration - 1000);
    },
    isRunning ? 1000 : null
  );

  useInterval(() => {
    if (countdownDuration < 1000) {
      setIsRunning(false); // stop running countdown on the last second
    }
  }, 1000);

  function handleChangeCountdown() {
    setIsRunning(!isRunning); // Reverse the state of isRunning on each button click
  }

  return (
    <div>
      <CountdownClock
        countdownDuration={countdownDuration}
        initDateTime={initDateTime.current}
      />
      {countdownDuration < 1000 && <h2>Countdown Reached!</h2>}
      <button onClick={handleChangeCountdown}>
        {isRunning ? "Pause Countdown" : "Continue Countdown"}
      </button>
    </div>
  );
}

Since we don't need to update the state of the initial date, and we would like its value to remain consistent through each render, a useRef is appropriate here as it functions as an instance variable in a class.

Here is the full Countdown.js file if you didn't want to click over to the Code Sandbox Example:

Countdown.js

import React, { useEffect, useState, useRef } from "react";
import CountdownClock from "./CountdownClock";

function useInterval(callback, delay) {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  });

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

function Countdown() {
  const initDateTime = useRef(Date.now()); // since we don't need to change this value, we can use a ref
  const [countdownDuration, setCountdownDuration] = useState(
    initDateTime.current + 123456789 // Random number choosen for this
  );

  const [isRunning, setIsRunning] = useState(true);

  useInterval(
    () => {
      setCountdownDuration(countdownDuration - 1000);
    },
    isRunning ? 1000 : null
  );

  useInterval(() => {
    if (countdownDuration < 1000) {
      setIsRunning(false);
    }
  }, 1000);

  function handleChangeCountdown() {
    setIsRunning(!isRunning);
  }

  return (
    <div>
      <h1>Learning React Hooks</h1>
      <CountdownClock
        countdownDuration={countdownDuration}
        initDateTime={initDateTime.current}
      />
      {countdownDuration < 1000 && <h2>Countdown Reached!</h2>}
      <button onClick={handleChangeCountdown}>
        {isRunning ? "Pause Countdown" : "Continue Countdown"}
      </button>
    </div>
  );
}

export default Countdown;

If you are interested in how the CountdownClock component is defined, here it is below, but it's not nearly as important to the subject matter of hooks as the previous two code blocks are on the definition of the useInterval custom hook and the use of it in the Countdown component.

CountdownClock.js

import React from "react";
import moment from "moment";

function getPluralOutput(duration, name) {
  return duration > 0 && duration < 2
    ? `${duration} ${name}`
    : `${duration} ${name}s`;
}

function CountdownClock({ countdownDuration, initDateTime }) {
  const initMoment = moment(initDateTime);
  const currentDuration = moment(countdownDuration);
  const duration = moment.duration(currentDuration.diff(initMoment));
  // Get initial values
  let w = duration.weeks();
  let d = duration.days();
  let h = duration.hours();
  let m = duration.minutes();
  let s = duration.seconds();

  // Set display values
  w = getPluralOutput(w, "week");
  d = getPluralOutput(d, "day");
  h = getPluralOutput(h, "hour");
  m = getPluralOutput(m, "minute");
  s = getPluralOutput(s, "second");

  return <h3>{`${w} ${d} ${h} ${m} ${s}`}</h3>;
}

export default CountdownClock;

Resources

While you can simply start using React Hooks if you have a project with React v16.8 or higher, this is modern JavaScript, so you know there is going to be some tooling to help guide syntax and best practices.

The React team recomments using their eslint-plugin-react-hooks package with the exhaustive-deps rule. If you are using Create React App (CRA), this plugin was included in react-scripts v3.0.0 or higher.

Whether using CRA or not, if you want these eslint rules to display in your editor of choice instead of just the terminal or in-browser (with CRA), you will still need to create an .eslintrc.json (or .eslintrc) file in your project and have the proper eslint plugin or extension for your editor.

{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

Learn more about React Hooks