The five React hooks you must absolutely learn as a beginner

The five React hooks you must absolutely learn as a beginner


React Hooks are a curious case. They didn’t bring anything new to React that it couldn’t do before. But they have made developers’ lives a lot easier and less confusing since their introduction back in 2019.

Today, hooks have become one of the biggest buzzwords in React that any beginner wouldn’t fail to notice. This popularity is not without reason.

Hooks gave us a new way to use state and lifecycle features in React inside function components. It was an ability limited to class components before hooks. This allowed us to create React components more intuitively using less code. It even made learning React a lot less complex for beginners.

In simpler terms, React hooks are a set of methods that allows you to use certain React features inside functions, something that was impossible before. Today, React comes with over a dozen built-in hooks and the ability to create custom hooks as needed.

Among them, there are five hooks that we think every beginner should know to get the maximum out of React. In this post, we’ll go through each of the five and show you how to use them when creating your function components.


What are React Hooks?

Before hooks were introduced to React in version 16.8, you had to develop components as classes if you wanted them to take certain actions. To be exact, these are the actions that you should execute at different stages of a component lifecycle .

For example, think of instances where you have to give the component a state or populate it with data fetched from the backend.

Classes allowed us to do these things easily because their instances can last throughout the application’s runtime once created. It allowed them to carry stateful information about components without a problem.

But functions are different from classes. They can’t hold stateful information because a function has to release all the information it gathers every time it returns. There’s usually no way to persist stateful information from one function call to another.

But React hooks changed this. They allowed function components to hold stateful information and imitate lifecycle behavior like a class component.

Now we can give your function components a state or use lifecycle methods during mounting, updating, or unmounting stages, thanks to hooks.

So, let’s familiarize ourselves with five of the most important hooks that allow us to create powerful components using functions.


useState — give your component a state

In React, state holds certain information about the component that persists between re-renders. Any change in the state prompts the component to re-render as well.

useState hook is used to give a state to a function component.

When called, useState creates a state variable along with its setter. While the variable itself is used to hold the state, we can use this setter to change its value. Calling the setter also re-renders the component.

However, the new and older values of the state variable have to be different to kick-start this re-rendering. React uses Object.is Javascript function to compare the two values to detect such a difference.

So, if the state stores a mutable data type, like objects or arrays, simply updating its value won’t prompt a re-rendering. That’s why it’s always a good practice to create an entirely new object when modifying a mutable data type.

To create state variables with useState, first, we have to import the hook to our application.

import { useState } from 'react';

We can pass the initial state value when calling useState. It returns the variable and its setter, like in the below example.

const [count, setCount] = useState(0);
const [name, setName] = useState('sam');
const [isFilled, setFilled] = useState(false);
const [person, setPerson] = useState({name:"sam", age:12, city:"london"});
const [items, setItems] = useState([1, 2, 4]);

Now, whenever there’s a change in one of the states, we can call its setter to update the value.

To understand this, let’s create a simple counter component that increments its value by one every time we click a button.

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

  return (
    <div>
      <h3>Count: {count}</h3>
      <button onClick={() => setCount(count + 1)}>
        Click me!
      </button>
    </div>
  );
}

Here, we have attached an onClick listener to the button. It runs the setCount method to increment the count by one. This prompts the Counter component to re-render every time the button is clicked and show the new state value.

Note that setCount method updates the state asynchronously. If you access the count variable immediately after calling setCount in the onClick handler, it won’t show you the new value but the old one.


useEffect — perform side effects from a component

useEffect hook gives you the ability to perform side effects from a function component. Side effects are actions that you should execute after the component finishes rendering—for example, tasks like fetching data or manually modifying the DOM.

If you’re familiar with class components, useEffect is the substitute for work done through lifecycle methods like componentDidMount, componentWillUnmount, and componentDidUpdate.

However, with useEffect, you don’t have to distinguish between a component mounting or updating. It runs the specified routine after every rendering by default.

Unlike in class components, this prevents you from repeating code for actions that runs after initial mounting process and subsequent updates. (In class components, you have to run the same code inside both componentDitMount and componentDidUpdate methods for actions like this.)

You can import useEffect to use it in the application similar to how we did with useState.

import { useEffect } from 'react';

We call useEffect from the function component to ensure it has access to state variables. This allows useEffect to change the component state if the side effect requires it.

useEffect accepts two arguments. The first one is the function that carries out the side effect. The second is an optional array of variables that controls exactly when to run the effect. If you don’t pass this second argument, the effect executes after every rendering process.

Let’s see how we can set up useEffect to log the current value of the count state to the console.

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

  useEffect(() => {
    console.log(count);
  });

  return (
    <div>
      <h3>Count: {count}</h3>
      <button onClick={() => setCount(count + 1)}>
        Click me!
      </button>
    </div>
  );
}

Now, every time we click the button, the Counter component re-renders and logs the value of count to the console.

However, if we’re using useEffect to fetch data from an API, running useEffect this way after every re-rendering is counterproductive. It’s enough to run the fetch operation only after the initial mount in such cases.

How do we model this with useEffect?

We can use the hook’s optional array parameter to control when it should run. useEffect watches the variables in this array for any changes in their values. It doesn’t run the effect function if no changes are detected. For example, let’s consider the following modified Counter.

function Counter() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(100);

  useEffect(() => {
    console.log(count1, count2);
  }, [count2]);

  return (
    <div>
      <h3>Count 1: {count1}</h3>
      <h3>Count 2: {count2}</h3>
      <button onClick={() => setCount1(count1 + 1)}>
        Increment count 1
      </button>
      <button onClick={() => setCount2(count2 - 1)}>
        Decrement count 2
      </button>
    </div>
  );
}

It keeps track of two count variables, but we only pass one of them to the useEffect array. Because of this, logging now happens only when we click the second button, not the first.

So, if we want to run the effect only after the mount time, we can pass an empty array to the hook. Since it guarantees that there’ll be no changes in the array between re-renders, we can ensure data fetching happens only one time.

import Axios from 'axios';

function BookList() {
  const [books, setBooks] = useState([]);

  useEffect(() => {
    Axios.get("https://fakerapi.it/api/v1/books?_quantity=4").then(res => {
      setBooks(res.data.data);
    });
  }, []);

  return (
    <div>
      <h3>Book List</h3>
      <ul>
        {books.map((book) => (
          <li key={book.id}>{book.title} by {book.author}</li>
        ))}
      </ul>
    </div>
  );
}

Cleaning up with useEffect

Some effects we run with useEffect require a cleanup if it isn’t going to be used any longer. For example, if we’ve subscribed to a WebSocket, we should unsubscribe from it before unmounting the component. Otherwise, it causes memory leaks in the application.

Memory leaks happen when we forget to release the memory allocated to a process that is no longer in use. It causes performance issues in the app over time and lead to bugs. So, useEffect is created to be able to run cleanup operations when a component unmounts to avoid problems like this.

To enable useEffect to clean up after an effect, you must return a function specifying the steps necessary to release the allocated memory from the effect function.

That means, if you subscribed to a WebSocket through useEffect, you should return a function that unsubscribes from it in the effect function. Let’s see how it works with an example.

import io from 'socket.io-client';

const socket = io();

function Chat() {
  useEffect(() => {
    socket.on("connect", () => {
      console.log("Socket connected!");
    });

    socket.on('message', (data) => {
      console.log(data.message);
    });

    return () => {
      socket.off("connect");
      socket.off("message");
    }
  });
}

Here, we return a cleanup function that turns off listeners to connect and message events from the socket. React calls this function whenever the component unmounts. It also runs a clean-up before every re-rendering process.


useReducer — handle complex state logic

useReducer is sort of an alternative to useState. But React recommends it for use cases with complex state logic that depends on multiple sub-values or previous state information. This allows us to separate the code that manages the state from the code that handles rendering.

useReducer accepts three arguments: a reducer function, an initial state, and an init function. Using these inputs, it returns two outputs: the state variable and a dispatcher function.

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

Let’s understand what each of them is first.

  • Reducer: This function carries the logic to return the new state based on two arguments: state and action. State stores the current state while the action object holds information needed for determining the next state.
  • initialState: This is an optional argument that refers to the initial value of the state.
  • init: An optional function we can use to lazily initialize the state. When called, it sets the initial state with the help of the provided initialState value.
  • state: The state variable.
  • dispatcher: The function used to dispatch action objects.

We’ll try to understand how useReducer works in action using a modified Counter component. This Counter allows users to increase, decrease, or reset the count through different button clicks.

import {useReducer} from 'react';

const initialState = {count: 0};

function reducer(state, action) {
  let newState;

  switch (action.type) {
    case 'Increment':
      return {count: state.count + 1};
    case 'Decrement':
      return {count: state.count - 1};
    case 'Reset':
      return {count: 0};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatcher] = useReducer(reducer, initialState);

  return (
    <div>
      <h3>Count: {state.count}</h3>
      <button onClick={() => dispatcher({type: 'Increment'})}>
        Increment
      </button>
      <button onClick={() => dispatcher({type: 'Decrement'})}>
        Decrement
      </button>
      <button onClick={() => dispatcher({type: 'Reset'})}>
        Reset
      </button>
    </div>
  );
}

Above example shows us how to use the reducer to determine the next state using the current one and dispatch relevant action objects using the dispatcher. Calling the dispatcher triggers the reducer function, and when the reducer returns, React re-renders the component if the state has changed.

We can modify the above code to lazily initialize the state instead of directly passing it.

import {useReducer} from 'react';

const initialState = 0;

function init(initialState) {
  return {
    count: initialState,
    reset: true
  };
}

function reducer(state, action) {
  let newState;

  switch (action.type) {
    case 'Increment':
      return {
        count: state.count + 1,
        reset: false
      };
    case 'Decrement':
      return {
        count: state.count - 1,
        reset: false
      };
    case 'Reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatcher] = useReducer(reducer, initialState, init);

  return (
    <div>
      <h3>Count: {state.count}</h3>
      <button onClick={() => dispatcher({type: 'Increment'})}>
        Increment
      </button>
      <button onClick={() => dispatcher({type: 'Decrement'})}>
        Decrement
      </button>
      <button onClick={() => dispatcher({type: 'Reset', payload: initialState})}>
        Reset
      </button>
    </div>
  );
}

useRef — create references that persist between re-renders

useRef’s job in React is pretty simple. When called, it returns a mutable object that can persist between re-renders. That’s it. But this property of useRef makes it a good candidate for working as a reference that doesn’t burden us with pre-set rules.

As an example, let’s consider useState and useReducer. Even though they allow us to persist certain information between updates, they force the component to re-render whenever the variable value changes. useRef, however, doesn’t impose such conditions and allows us to modify the referenced object without following it with built-in procedures.

So, if you want to store stateful information about a component without forcing it to re-render with state changes, you should go for useRef. Let’s import and initialize the hook to see how that works.

import { useRef } from 'react';

function Counter() {
  const countRef = useRef(0);

  //rest of the component logic
}

We can call useRef with an initial value to create a reference to the count. useRef stores the passed value in its only property, current. We can read or modify the referenced value by accessing countRef.current. But it doesn’t cause the Counter to re-render even if the value updates.

So, in this example, even though the value logged to the console increase with every button click, the count displayed on the page stays the same.

import { useRef } from 'react';

function Counter() {
  const countRef = useRef(0);

  const handleClick = () => {
    countRef.current = countRef.current + 1;
    console.log(countRef.current);
  }

  return (
    <div>
      <h3>Count: {countRef.current}</h3>
      <button onClick={handleClick}>Click me!</button>
    </div>
  );
}

useRef as a reference to DOM elements

A second use case of useRef is accessing the DOM. We can pass a reference created by the hook to a DOM element. This allows us to directly access that element without Javascript selectors and manipulate it as we prefer.

Let’s see an example of this.

function TextBlock() {
  const paraRef = useRef("");

  return (
    <div>
      <h3>Welcome to React Hooks!</h3>
      <p ref={paraRef}>React hooks are awesome.</p>
    </div>
  );
}

After setting paraRef as the paragraph element’s reference, we can now access it through paraRef.current. It gives us a quick way to modify the element, like changing the text, applying a new color, or adding a strikethrough.

function TextBlock() {
  const paraRef = useRef("");

  useEffect(() => {
    paraRef.current.innerHTML = "Changed text with useRef";
  });

  const changeColor = () => {
    paraRef.current.style.color = "red";
  }

  const addStrikethrough = () => {
    paraRef.current.style.textDecoration = "line-through";
  }

  return (
    <div>
      <h3>Welcome to React Hooks!</h3>
      <p ref={paraRef}>React hooks are awesome.</p>
      <button onClick={changeColor}>Change text color</button>
      <button onClick={addStrikethrough}>Add a strikethough</button>
    </div>
  );
}

useMemo — memoize results from expensive calculations

useMemo hook gives us a way to prevent rerunning expensive calculations every time the component renders. It uses a technique called memoization to achieve this.

Memoization is a performance optimization tactic that prompts expensive calculations to return a cached result if the inputs haven’t changed since the last run. useMemo relies on this technique to reduce expensive recomputations to help your application perform better.

This hook accepts two arguments. First, a function that conducts the calculation; second, an array of dependencies used to determine whether it should recompute the results. useMemo reruns the calculation function only if there are any changes in the dependencies in this array. Otherwise, it returns the results cached from a previous calculation.

If no dependency array is provided, useMemo reruns the calculation during every rendering process.

Let’s say you have a component that displays the nth fibonacci number. This calculation heavily impacts the application’s performance as the value of n increases. So, we can turn to useMemo to prevent the component from recomputing the value when n hasn’t changed.

import { useMemo } from 'react';

function calculateFibonacci(num) {
  if (num === 0) {
    return 0;
  } else if (num === 1) {
    return 1;
  } else {
    return calculateFibonacci(num - 1) + calculateFibonacci(num - 2);
  }
}

function FibonacciCalculator() {
  const [num, setNum] = useState(0);
  const [int, setInt] = useState(0);

  const fibonacci = useMemo(() => {
    const value = calculateFibonacci(num);
    console.log(value);
    return value;
  }, [num]);

  return (
    <div>
      <h3>{num + 1}th Fibonacci number is {fibonacci}</h3>
      <button onClick={() => setNum(num + 1)}>Increment N</button>
      <button onClick={() => setInt(int + 1)}>Re-Render</button>
    </div>
  );
}

Despite its benefit, useMemo is a hook you should use with caution. It can easily have the opposite effect of a performance gain if you’re not careful. As a rule of thumb, apply useMemo only when it can bring a significant performance gain to your app. If the gain is marginal, you’d be better off with a regular implementation than with useMemo.

Thanks for reading!