Most Common React Hooks Explained

react
frontend

When trying to pick up a new technology, it can get extremely overwhelming with the amount of new concepts and new terminology thrown at you. That was my experience, back in college, when I was learning React. But just as I was starting to get somewhat comfortable with this new framework, a big shift started happening in the community: everyone was moving from Class components to Functional components. Not wanting to be left behind, I jumped on the bandwagon and made the switch, but … it was not as straight forward as I would have hoped. One of the first overwhelming concepts for me was wrapping my head around Hooks. This was a strange pattern and way of thinking for me at that time and, if you share the same feelings, hopefully by the end of this article you will feel a lot less overwhelmed and more confident about diving deeper into React.

What are React Hooks?

Before getting too deep into the explanation, if the tutorial you’ve been following to learn React is using classes then hooks won’t apply. Although I suggest switching over to using Functional components instead of Class components, which I will go over in a separate Class vs Functional components article, React Hooks are specific to Functional components.

So what are React Hooks? The simplest way to think about them is just as functions that allow for different behaviour you might want to use in your components. There are plenty of these functions available for “hooking into” native React functionality but it is also possible to create your own hooks (which we won’t go into in this article). Some of these hooks available include useState , useEffect , useMemo and lot more. One thing you might have notices is that the names have to start with the word use. Let’s look a bit further into why you might want to use these hooks …

Why use React Hooks?

When looking at Class components, we have access to use things like state (data the component remembers and shows on the screen) and lifecycle methods (code that runs at specific times in a component’s life, like when the component is first rendered on the screen). How do we use all of these with Functional components? This is where Hooks come into play.

Simple

Like mentioned above, hooks give us access to a bunch of React functionality but the difference here is that it is done in a more straightforward and intuitive way compared to Class components.

Reusable

It is a lot easier to share logic between components without the need of complex patterns like High-Order Components or Render Props.

Organized

All logic related to a particular piece of state or effect is organized a lot better inside a component which makes the code a lot cleaner and easier to understand.

For beginners, it is often easier to start with Functional components and Hooks as opposed to starting with Class components. The code will also become a lot more consistent as hooks encourage small and reusable functions, less boilerplate code and more straightforward/easy to understand code.

Template

Before going too deep into explaining each of the most used hooks, lets take a moment to define a basic component that we can use across all examples. Having a stable foundation that doesn’t change from example to example will allow us to just focus on what matters.

Let’s create a simple Counter component. I know, I know … a pretty lame example but it does a pretty good job showcasing all the hooks. At its core, the component should either increment a counter or display a counter .

const Counter = () => {
  return (
    <div>
      <h1>Counter</h1>
      <button>Increment</button>
    </div>
  );
};

The example is pretty straight forward as it’s just a component returning a div with a header and a button inside.

Most used React Hooks

useState

This is by far the hook you will end up using the most. As the name suggests, this hook is used for adding state to functional components. Depending on your coding background, the word state could mean a lot of different things, but with react it simply just refers to data that the component can hold and change over time.

import React, { useState } from 'react';
 
const Counter = () => {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

Ok, there are quite a few things that changed with this hook. Let’s break it down line by line to see what each does.e

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

Whenever we use useState it provides us with 2 variables for the state we are looking to track. In this case, we are looking to store the count in state and what we get back from the hook are the value and the setter. The names can be anything we want but should be named according to what is being stored.

count: the value of the counter setCount: the setter function for the counter. Whenever we want to update count we need to call this function and pass in the new value Another convention a lot of people follow is prefixing the setter with the word set. In this case we have setCount.

But why do we need this state? Why not just use a regular variable? Regular variables don’t trigger re-rendering of the component when the values change. React components re-render when their state or props change which we don’t get with regular variables.

Another small detail you might have missed in the line above is the 0 that is being passed to useState. This is the default value for our count.

<h1>Counter: {count}</h1>

Although this is an article on hooks and not state, I just wanted to point out that this is where the magic happens with state. Had we used a simple variable, when displaying the count, incrementing it would not show the updates on the UI.

<button onClick={() => setCount(count + 1)}>Increment</button>

As mentioned before, when we want to update the count we need to use the function provided by the hook to do so. Without going too much into it, one of the main reasons for calling this function is it signals React to schedule a re-render of the component with the new changes.

A mistake I’ve seen a lot of Junior Developers make is assuming updating the state is a synchronous change. State updates are asynchornous in React. When you call the setter function, React doesn’t immediately re-render the component. Instead, it marks the component as needing an update in order to perform efficient batching. This can result in a race condition if we try to set the state and access the state immediately right after in the same function.

useEffect

The second most popular hook you will come across is useEffect. It is used to perform all sort of side effects. A few examples of side effects include operations like data fetching, subscriptions, or manually changing the DOM, which are not directly related to the output of the component.

An easy way to think about this hook is like setting up a watchman or a lookout. This watchman keeps an eye on something, and when that something changes, it performs a task.

import React, { useState, useEffect } from 'react';
 
const Counter = () => {
  const [count, setCount] = useState(0);
 
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  }, [count]);
 
  return (
    <div>
      <h1>Counter: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

As you can tell, we are still using the useState hook from the previous step. You will notice this across most examples as this is a core hook which gets used a lot.

In this scenario, we are using the useEffect hook to update the document title whenever the count changes. Let’s break this down line by line so that it hopefully makes a bit more sense.

document.title = `You clicked ${count} times`;

Pretty straight forward here. We set the title of the document using the state value of the count.

useEffect(() => {
  /* logic goes here */
}, [count]);

The parameters for this hook can be a little tough to wrap your head around at first but once you do, it will make a lot of sense.

  • arrow function: the first parameter takes in an arrow function. This is where the logic that we want executed every time a change is detected lives.
  • array: the second parameter takes in an array of dependencies. This array tells React when to re-run the effect. The elements are typically state variables or props. Passing in an empty array [] will only run the effect once when the component mounts. If you omit the array completely, the effect will run after every render of the component.

Mounting is a term used in React to describe the process of creating and inserting a component into the DOM. Mounting is like putting a picture up on a wall. It also only happens once per component instance, while rendering can happen many times.

useContext

When it comes to useContext, this hook will be a bit tougher to explain without some existing knowledge about how the React context works (which I will save for a separate article as this is an extensive topic).

Normally, in React, if you want to pass data from one component to another, you pass it down as props. This is fine for a small number of components, but for deeper component trees, it becomes cumbersome. This is often referred to as prop drilling — where you pass props through components that don’t necessarily need them, just to get them to components further down the tree.

The purpose behind this hook is to allow us to use the React context in order to share data across a section or the entire component tree. Generally, you would wrap an area of the code in a Provider component which will, you guessed it, provide the context data to all of the components it is wrapping.

Let’s create a context for the theme of our app. This theme can then be passed to our component in order to adapt to the theme of the rest of the app.

import React, { useContext } from 'react';
 
const ThemeContext = React.createContext();
 
const App = () => (
  <ThemeContext.Provider value="light">
    <Counter />
  </ThemeContext.Provider>
);
 
const Counter = () => {
  const theme = useContext(ThemeContext);
 
  return (
    <div className={theme}>
      <h1>Counter, Using {theme} theme</h1>
    </div>
  );
};

First, we need to create our React context. We do this by calling React.createContext(). In our case, we called it ThemeContext as this is where we are keeping the theme for our app.

const ThemeContext = React.createContext();
 
const App = () => (
  <ThemeContext.Provider value="light">
    <Counter />
  </ThemeContext.Provider>
);

Normally, whenever you set up the Provider for the context, you might also want to store the data in state. In this case, for simplicity sake, we are just hard coding the value to light which will set the app to light theme.

By wrapping the Counter component in the Provider of the ThemeContext, this will give us access to the theme value inside of the component. For this to be a lot more beneficial we would have to wrap the Provider around more than just one component, but in our case this is all we have. The real benefits of using a context are seen with a slightly more complicated application.

const theme = useContext(ThemeContext);

We now finally get to use the useContext hook inside of our component. This will simply just grab the theme value from the context we set up. Pretty sweet!

useReducer

Not nearly as common as the other hooks on this list, but still a good option to know about is useReducer. It is an alternative to the useState hook but where it shines is managing more complex state logic.

import React, { useReducer } from 'react';
 
const counterReducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    default:
      throw new Error('Action type not supported.');
  }
};
 
const Counter = () => {
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });
 
  return (
    <div>
      <h1>Counter: {state.count}</h1>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
    </div>
  );
};

Although our example is very simple, this is enough to show the strengths of the hook. Let’s look at what is happening here.

const counterReducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    default:
      throw new Error('Action type not supported.');
  }
};

This is the handler for our state. It receives the current state and the action we want to perform on the state. In our case, it handles an increment action where it takes the current count and adds 1 to it. You can see how this could easily be expanded to support other actions like decrement.

const [state, dispatch] = useReducer(counterReducer, { count: 0 });

When we set up the hook, it requires two things: the reducer and initial state. The reducer is the function we went over in the previous step and the initial state is an object. This is the major difference with useReducer hook versus the useState one. This hook is particularly well-suited for managing complex state that involves multiple sub-values.

The other thing to mention is the return of useReducer. Now, instead of receiving the state and the setter, we receive the state (the entire object) and a dispatcher. The dispatcher is how we send our actions to the reducer.

<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>

Our onClick handler changed from our previous examples. Now instead of simply just setting the state to the new value, it dispatches an event with the increment type. This will get to our reducer function, check for the increment type, and update our state. We get to the same outcome but where this is extremely useful is if our state was a lot more complex and we wanted to manage different actions. Try taking this example and seeing if you can easily add another action for decrementing the count.

useRef

I like to think of useRef as a box in which you can store something and have the option to reach in and grab what you stored whenever you need it. The contents also won’t change unless you explicitly decide to change them.

So far, you might be saying to yourself, “Isn’t this just like state?”. There are a few key differences here. One of them being that we can store a DOM element which allows us to do things like setting focus on that element. The other is that the value stored stays the same between re-renders. Updating the value also does not trigger a component re-render unlike state created through useState.

import React, { useState, useRef } from 'react';
 
const Counter = () => {
  const [count, setCount] = useState(0);
 
  const nameInput = useRef(null);
 
  const onIncrementClick = () => {
    setCount(count + 1);
    nameInput.current.focus();
  };
 
  return (
    <div>
      <input ref={nameInput} type="text" />
 
      <h1>Counter: {count}</h1>
      <button onClick={onIncrementClick}>Increment</button>
    </div>
  );
};

In this example, we are reusing the useState example but each time the value is incremented, we switch focus to an input field.

const nameInput = useRef(null);

We need to initialize useRef in order to hold onto our text input element. For the default value of our ref, we will just set it to null. When we do this, nameInput.current will be null.

<input ref={nameInput} type="text" />

Assigning our nameInput ref to the ref property on an element will do two things:

  • React will update nameInput.current to now store the DOM element for the input
  • When the element is removed from the DOM, React will set nameInput.current back to null in order to prevent memory leaks or references to detached DOM nodes
const onIncrementClick = () => {
  setCount(count + 1);
  nameInput.current.focus();
};

We can now set focus on our element whenever we increment the count value. To achieve this, we pulled the logic for the incrementing into its own handler function. This will clean up the code a bit since there is a lot more logic we need to deal with.

There are a lot more things we can do with a DOM element than just setting focus, it all depends on what we are trying to achieve.

useMemo

We are now entering the realm of performance optimizations. Let’s say you have a function that computes a value based on a couple parameters you pass in and this computation can take a while to resolve. Having to re-calculate on every re-render when the parameters don’t change can result in a performance loss.

This is the problem useMemo is tackling. We are able to save the result of a function and only re-calculate the output if the parameters change. This can improve performance, especially for expensive calculations. The way it is written is also similar to useEffect so the structure is consistent here.

import React, { useState, useMemo } from 'react';
import { computeExpensiveValue } from './utils';
 
const Counter = () => {
  const [count, setCount] = useState(0);
 
  const expensiveResult = useMemo(() => {
    return computeExpensiveValue(count);
  }, [count]);
 
  return (
    <div>
      <h1>Counter: {count}</h1>
      <div>Result: {expensiveResult}</div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

Let’s say we have a function computeExpensiveValue which we are importing from our utils. This function takes in a count and does some computations behind the scenes that are quite expensive. If we want to only re-calculate the result of calling this function when the value of count changes we have to do the following, as seen in the code above:

const expensiveResult = useMemo(() => {
  return computeExpensiveValue(count);
}, [count]);

The first parameter is an arrow function that calls our expensive function with the parameters we want. The second parameter is a dependency array, just like how we had with useEffect. When any of the values in the dependency array change it will trigger a call to re-compute the value

The goal should not be to simply just wrap every function call in a useMemo. It should be applied with a specific purpose and understanding of its impact. These optimizations come with an overhead and that overhead can outweigh the benefits of the hook if used with trivial computations.

useCallback

Another optimization hook, useCallback, is a way to remember a function so that the same function instance is re-used between re-renders of components, instead of creating a new function each time.

What do I mean by creating a new function each time? In React, functions are re-created on every render. This is generally fine, but in some cases, especially when a function is passed as a prop to a child component, this can lead to performance issues. Even though the function does the same thing, the signature of it changes with every render which can make child components re-render unnecessarily.

One of the most obvious scenarios where useCallback should be used is when using a function in the dependency array of other hooks like useEffect. If the signature changes on every re-render, this can result in the hook firing unexpectedly. Other scenarios where the hook should be considered are passing functions down to child components or the re-creation of the function on every render being a performance concern. Again, just like useMemo, there is an overhead when using this hook so you shouldn’t just wrap any function you see without proper consideration first.

import React, { useState, useCallback } from 'react';
 
const Counter = () => {
const [count, setCount] = useState(0);
 
const onIncrementClick = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []);
 
return (
 
<div>
  <h1>Counter: {count}</h1>
  <button onClick={onIncrementClick}>Increment</button>
</div>
) } As you can see, the structure of things is similar to that of useMemo but with
a few tweaks (and obviously using a different hook completely).
 
const onIncrementClick = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []);

This is not the best use of this hook as we are only using this function for an onClick handler, but it is good to visualize how it works. The first parameter is the function we are looking to memoize. The second parameter is the dependency array. In this scenario, the function does not change so we don’t need any dependencies (the function will only get created once).

Another thing you might have noticed is for setCount we are not calling it with the usual count + 1 but instead are providing an arrow function. When the new state is derived from the old state, this is a reliable way to ensure we are working with the most current state value at the time of calling the function.

Conclusion

This article ended up being a lot longer than I had planned. Although I could have cut out a lot of detail, I believe in the end it all paints a fuller picture. The goal was not to simply just skim over every hook, give a couple bullet points for each and move on. Instead, it was to try and write it in a way where if I were to start all over, learning React, I would answer most/all of the questions I would have. Some knowledge about React basics was assumed, otherwise answering ALL questions and going in depth about everything would have probably at least doubled the length.

If you found this useful, check out my other articles and my YouTube Channel where I cover a lot of the same topics but in video format.