React Hooks - explained

React Hooks - explained

React Hooks were introduced in React 16.8 which allows functional components to have access to state and other features like performing an after effect when a particular condition is met or specific changes occur in the state(s) without having to write class components.

In this article, I will explain how to use React Hooks (useState, useEffect, useContext, useRef, useReducer, useCallback, useMemo) and how to create your custom Hooks. Just bear in mind that you can use hooks solely for the functional component.

What is a Hook in React?

A hook is a special function that enables the use of state and several other features in functional components available in class-based components.

The rules of a Hook

Let's talk about the general rules of Hooks. It is important to know where in your code you can use hooks. You need to follow the rules to use Hooks.

  1. Hooks can only be called inside React function components.
  2. Hooks can only be called at the top level of a component.
  3. Hooks cannot be conditional.

React useState Hook

Your application's status will undoubtedly change at some point. This might be any form of data present in your components, such as the value of a variable or object.

The React useState hooks allow us to track and reflect these changes in the DOM.

Let's see how can we use useState hook in a function component.

To use this hook, we first need to import it into our component.

import {useState} from 'react'

Next, we need to initialize our state by calling useState. It accepts an initial state and returns two values.

  • The current state
  • A function that updates the state
const [name, setName] = useState('John Doe');

We are destructuring the returned values from useState. The first value is a name which is our current state and the second value setName is the function that is used to update our state. Lastly, we set the initial state to John Doe.

Our state is initialized and ready to be used anywhere in our component. For example, we can read the state by using the state variable in the rendered component.

import { useState } from 'react';
function MyComponent() {
  const [name, setName] = useState('John Doe');
   return (
    <>
      <div>
        <p>My name is {name}</p>
      </div>
    </>
  );
}
export default MyComponent;

Similarly, we can update our state by using the state updater function.

import { useState } from 'react';
function MyComponent() {
  const [name, setName] = useState('John Doe');
  const changeName = () => {
    setName('Foo');
  };
  return (
    <>
      <div>
        <p>My name is {name}</p>
        <button onClick={changeName}> Click me </button>
      </div>
    </>
  );
}
export default MyComponent;

In the code above, we update the state with a button click.

At this point you might be thinking, what can the state hold?🤔

The useState hook can keep track of objects, arrays, strings, numbers, booleans, and any combination of these.

React useEffect Hook

I have written a separate post on React useEffect Hook

React useContext Hook

React useContext hook is a way to manage states globally no matter how deep they are in the components tree.

In React, there are three basic steps to using the context:

  1. creating the context
  2. providing the context
  3. consuming the context.

But, before diving into details on the above steps, let's talk about what problem does useContext hook solves.

The short answer is props drilling.

The parent component in the stack that requires access to the state should hold the state.

For example, let's say we have many nested components and access to the state is required by the components at the top and bottom of the stack.

We will need to pass the state as "props" through each nested component to accomplish this without Context. This is called "prop drilling". Let's see an example.

react-useContext-prop-drilling
Passing "props" through nested components:

As you can see, components 2 and 3 did not need the state, even though they had to pass the state along so that it could reach component 4.

So, the solution to this problem is to use the useContext hook.

The main goal of using the context is to provide your components access to some global data and enable them to re-render if that global data is changed. When it comes to passing along props from parents to children, context is the key to solving the props drilling problem.

However, you should be careful before deciding to use context in your app as it adds complexity to the application and makes it hard to unit test.

A quick reminder, there are three basic steps to using the context, creating the context, providing the context, and consuming the context. Let's dive into more details about this and solve the props drilling problem.

Creating the context

To create context, you must import the built-in function createContext and initialize it.

import { createContext } from 'react';
const Context = createContext();

Providing the context

Context.Provider is used to provide the context to its child components, no matter how deep they are. You can use the  value prop to set the value of context.

function Component1() {
  const [user, setUser] = useState("John Doe");

  return (
    <UserContext.Provider value={user}>
      <h1>{`Hey ${user}!`}</h1>
      <Component2 user={user} />
    </UserContext.Provider>
  );
}

Now, the user Context will be accessible to every component in this tree.

Consuming the context

To use the Context in a child component, we need to access it using the useContext Hook.

import { useContext } from "react";

function Component4() {
  const user = useContext(UserContext);

  return (
    <>
      <h1>Component 4</h1>
      <h2>{`Hey ${user} again!`}</h2>
    </>
  );
}

The full example is below.

import { useState, createContext, useContext } from 'react';

const UserContext = createContext();

function Component1() {
  const [user, setUser] = useState('John Doe');

  return (
    <UserContext.Provider value={user}>
      <h1>{`Hey ${user}!`}</h1>
      <Component2 />
    </UserContext.Provider>
  );
}

function Component2() {
  return (
    <>
      <h1>Component 2</h1>
      <Component3 />
    </>
  );
}

function Component3() {
  return (
    <>
      <h1>Component 3</h1>
      <Component4 />
    </>
  );
}

function Component4() {
  const user = useContext(UserContext);
  return (
    <>
      <h1>Component 4</h1>
      <h2>{`Hey ${user} again!`}</h2>
    </>
  );
}

React useRef Hook

👉
The useRef Hook allows you to persist values between renders.

This hook also makes it possible to access DOM nodes directly within the functional components.

UseState and useReducer in a React component can cause your component re-render every time the update methods are called.

Let's take a look at how to use the useRef() hook to keep track of variables without causing re-renders, and how to enforce the re-rendering of React Components.

useRef does not cause re-renders

The useState The hook itself causes a re-render, therefore if we were to count how many times our application renders using this Hook, we would be caught in an infinite loop. The useRef hook can be used to avoid this.

useRef(initialValue) accepts one argument as the initial value and returns a reference. A reference is an object having a special property current.

import { useRef } from 'react';

export default function LogButtonClicks() {
  const countRef = useRef(0);

  const handle = () => {
    countRef.current++;
    console.log(`Clicked ${countRef.current} times`);
  };
  console.log('I rendered!');
  return <button onClick={handle}>Click me</button>;
}

const countRef = useRef(0) creates a references countRef initialized with 0.

CountRef.current++ is increased when the button is clicked, and the handle function is also invoked. The reference value is logged to the console.

Updating the reference value countRef.current++ does not cause component re-rendering.

As you can see, updating the reference value countRef.current++ does not cause component re-rendering, because you can see in the console that 'I rendered!' is logged just once at initial rendering and does not re-render when the reference is updated.

useref-hook

Now, let's see the difference between reference and state by re-using  LogButtonClicks from the previous example. We will use the useState hook to count the number of button clicks.

import { useState } from 'react';

export default function LogButtonClicks() {
  const [count, setCount] = useState(0);
  
  const handle = () => {
    const updatedCount = count + 1;
    console.log(`Clicked ${updatedCount} times`);
    setCount(updatedCount);
  };
  console.log('I rendered!');
  return <button onClick={handle}>Click me</button>;
}
usestate-and-useref-difference

As you can see every time the button is clicked, the message 'I rendered!' gets logged in the console causing the component to re-render.

Accessing DOM elements

Most of the time, we should let React handle all the DOM manipulation, but there might be some use cases where we want to take control of DOM manipulation. In such scenarios, we can use useRef hook to do this without causing any issues.  

For instance, let's say we want to focus on the input field when the component mounts. To make it work you’ll need to create a reference to the input, and assign the reference to the ref attribute of the input, once the component mounts call the element.focus() method on the element.

import { useRef, useEffect } from 'react';

function TextInputWithFocus() {
  const inputRef = useRef();
  useEffect(() => {
    inputRef.current.focus();
  }, []);
  return (
    <input ref={inputRef} type="text"/>
  );
}

The functional component's function scope should either calculate the output or invoke hooks. Because of this, updating a reference and changing the state of a component shouldn't be performed inside the immediate scope of the component's function.

Either a useEffect() callback or handlers (event handlers, timer handlers, etc) must be used to change the reference.

import { useRef, useEffect } from 'react';

function MyComponent({ prop }) {
  const myRef = useRef(0);
    
  useEffect(() => {
    myRef.current++; // Good!👍
    setTimeout(() => {
      myRef.current++; // Good!👍
    }, 1000);
  }, []);
    
  const handler = () => {
    myRef.current++; // Good!👍
  };
    
  myRef.current++; // Bad!👎
    
  if (prop) {
    myRef.current++; // Bad!👎
  }
  return <button onClick={handler}>My button</button>;
}

As we can persist useRef values between renders, we can use this hook to keep track of previous state values.

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

function MyComponent() {
  const [inputValue, setInputValue] = useState("");
  const previousInputValue = useRef("");

  useEffect(() => {
    previousInputValue.current = inputValue;
  }, [inputValue]);

  return (
    <>
      <input type="text" value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
      />
      <h2>Current Value: {inputValue}</h2>
      <h2>Previous Value: {previousInputValue.current}</h2>
    </>
  );
}
previous-state-values-useref-hook

React useReducer Hook

The useReducer Hook is similar to the useState Hook which also allows you to manage state and re-render a component whenever that state changes. The idea behind useReducer is it gives you a more concrete way to handle complex states. It gives you a set of actions that you can perform in your state and it's going to convert your current state to a new version of the state based on the action that you send it. If you're familiar with redux, the useReducer hook has a very similar pattern to redux.

useReducer(<reducer>, <initialState>)

The useReducer Hook accepts two arguments: reducer and initialState.

The hook then returns an array of 2 items: the current state and the dispatch function.

import { useReducer } from 'react';
function MyComponent() {
  const [state, dispatch] = useReducer(reducer, initialState);
    
  const action = {
    type: 'ActionType'
  };
  return (
    <button onClick={() => dispatch(action)}>
      Click me
    </button>
  );
}

Now, let's first understand what the terms initial state, action object, dispatch, and reducer mean.

Initial state: This can be a simple value the state is initialized with, but generally contain an object. For instance, in the case of user state, the initial value could be:

// initial state
const initialState = { 
  users: []
};

Action Object: It describes how to update the state. The property type of an action object is often a string defining the kind of state update that the reducer needs to do. For example

const action = {
  type: 'ADD_USER'
};

You can add more attributes to the action object if the reducer requires it to contain a payload.

const action = {
  type: 'ADD_USER',
  user: { 
    name: 'John Doe',
    email: '[email protected]'
  }
};

Dispatch function: It is a special function that dispatches an action object. It is created by the useReducer hook.

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

You can simply call the dispatch function with the appropriate action object: dispatch(actionObject) to update the state.

Reducer function: It is a pure function that contains your custom logic. It accepts 2 parameters: the current state and an action object. The reducer function must update the state in an immutable way, depending on the action object, and return the new state.

The following reducer function adds the user to the state.

const initialState = {
  users: [],
}

const userReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'ADD_USER':
      return {
        ...state,
        users: [...state.users, action.payload],
      }
    default:
      return state
  }
}

Wiring everything

import React, { useReducer } from 'react'

const initialState = {
  users: [],
}

const userReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'ADD_USER':
      return {
        ...state,
        users: [...state.users, action.payload],
      }
    default:
      return state
  }
}

function Reducer() {
  const [users, dispatch] = useReducer(userReducer, initialState)
  console.log(JSON.stringify(users, null, 2))

  const payload = {
    name: 'John Smith',
    email: '[email protected]',
  }

  const addUser = () => {
    dispatch({ type: 'ADD_USER', payload })
  }

  return (
    <div>
      <button onClick={addUser}>Add User</button>
    </div>
  )
}

export default Reducer

From the above code snippet, when you click the Add User button, the function dispatches an action object of the type ADD_USER which adds the user to the state and logs the new state in the console. You can add more complex logic like updating, and deleting a user inside the single reducer function by adding more actions.

useReducer-hook

React useCallback Hook

Let's first discuss performance optimization before moving on. The render time must be taken into account whenever we create a React app. We can enhance our application performance if we can reduce the time taken to render the DOM. One way to achieve this will be by preventing unnecessary renders of our components. This might not have great performance improvement for a small application, but if we have a large application with many components, we can have a serious performance boost 🚀.

The React useCallback returns a memoized version of the callback function that changes only when one of the dependencies has changed.

You can think of memoization as caching, so it returns a cached version of the result (callback function) which can boost the performance.

It is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.

Why We Need a useCallback Function

One reason to use the useCallback function is to prevent unnecessary re-rendering of the components. The component should not re-render unless its props have been changed.

Let's see an example.

import React, { useState } from 'react';
import User from './User';

function CallbackHookDemo() {
  const [users, setUsers] = useState([]);
  const [darkMode, setDarkMode] = useState(false);

  const addUser = () => {
    setUsers((u) => [...u, 'New User added !!']);
  };

  const handleCheckboxChange = () => setDarkMode((prev) => !prev);

  return (
    <>
      <User users={users} addUser={addUser} />
      <hr />

      <div className={darkMode ? 'dark-mode' : ''}>
        <label htmlFor="darkMode">dark mode</label>
        <input
          name="darkMode"
          type="checkbox"
          checked={darkMode}
          onChange={handleCheckboxChange}
        />
      </div>
    </>
  );
}

export default CallbackHookDemo;
import React, { memo } from 'react';

function User({ users, addUser }) {
  console.log('User component rendered');
  return (
    <>
      <h2>Users:</h2>
      {users.map((user, i) => {
        return <p key={i}>{user}</p>;
      })}
      <button onClick={addUser}>Add User</button>
    </>
  );
}

export default memo(User);
User.js

When you run this code and check the dark mode checkbox, you will notice that the User component re-renders even when the users do not change.

useCallback-hook

Why is this happening?🤔 The User component should not re-render since neither the users state nor the addUser function are changing when the checkbox is checked.

This is due to referential equality. Functions get recreated when a component re-renders. That's why the addUser function has changed.

To fix this, we can use the useCallback hook to prevent unnecessary re-render of the component unless necessary. We can now wrap the addUser function inside the useCallback hook like the below code.

  const addUser = useCallback(() => {
    setUsers((u) => [...u, 'New User added !!']);
  }, [users]);

Now, the User component will only re-render when the users prop changes.

React useMemo Hook

The react useMemo hook returns a memoized value that only runs when one of its dependencies changes.

💡
UseMemo and useCallback hooks have some similarities. The main difference is that UseMemo returns a memoized value whereas useCallback returns a memoized function.

You might have some expensive and resource-intensive functions in your React application. If those functions are run on every re-render of the component, the application will have a massive performance issue which can be costly in either memory, time, or processing and it will also lead to poor user experience.

You can try to optimize the performance of the application by utilizing the memoization technique.

useMemo hook tries to address the following two problems.

  • referential equality
  • computationally expensive operations

useMemo() is a built-in React hook that accepts 2 arguments — a function compute that computes a result and the depedencies array:

const memoizedResult = useMemo(compute, dependencies);

On the first render, useMemo(compute, dependencies) invokes compute, remembers the calculated output and returns it to the component.

In the subsequent renders, the compute functions would not need to run again as long as the dependencies do not change. It will simply return the memoized output.

Now let's see how useMemo() works in an example.

import React, { useState } from 'react'

const expensiveCalculation = (num) => {
  console.log('Calculating...')
  for (let i = 0; i < 1000000000; i++) {
    num += 1
  }
  return num
}

function UseMemoHookExample() {
  const [users, setUsers] = useState([])
  const [count, setCount] = useState(0)
  const calculation = expensiveCalculation(count)

  const addUser = () => {
    setUsers((u) => [...u, 'New User added !!'])
  }

  const increment = () => {
    setCount((c) => c + 1)
  }

  return (
    <>
      <h2>Users:</h2>
      {users.map((user, i) => {
        return <p key={i}>{user}</p>
      })}
      <button onClick={addUser}>Add User</button>
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
        <h2>Expensive Calculation</h2>
        {calculation}
      </div>
    </>
  )
}

export default UseMemoHookExample

In this example, you will notice that when you increase the count or add a user, there is a delay in execution.

Even if you only add a user, the component re-renders, and the expensive calculation function gets invoked resulting in a delayed execution.

Let's improve the performance of this component by using useMemo hook which will memoize the result of the expensive function.

import React, { useMemo, useState } from 'react'

const expensiveCalculation = (num) => {
  console.log('Calculating...')
  for (let i = 0; i < 1000000000; i++) {
    num += 1
  }
  return num
}

function UseMemoHookExample() {
  const [users, setUsers] = useState([])
  const [count, setCount] = useState(0)
  const calculation = useMemo(() => expensiveCalculation(count), [count])

  const addUser = () => {
    setUsers((u) => [...u, 'New User added !!'])
  }

  const increment = () => {
    setCount((c) => c + 1)
  }

  return (
    <>
      <h2>Users:</h2>
      {users.map((user, i) => {
        return <p key={i}>{user}</p>
      })}
      <button onClick={addUser}>Add User</button>
      <hr />
      <div>
        Count: {count}
        <button onClick={increment}>+</button>
        <h2>Expensive Calculation</h2>
        {calculation}
      </div>
    </>
  )
}

export default UseMemoHookExample

Now, when the component renders for the first time, an expensive calculation function will be invoked and the result will be memoized so that when we click on add user, the function will not be invoked again as there is no change in dependency and the execution will be super fast.

React custom Hook

You can create your custom hook by using the react built-in hooks and extract your custom logic into a reusable function.

There are two rules for creating a custom hook:

  • Custom hooks are prefixed with "use". For example, it could be named as useFetch
  • Custom hooks consist of built-in or other custom hooks. A custom Hook is not a custom Hook and should not begin with the prefix "use" if it does not internally utilize any hooks.

Let's create a custom hook that will fetch a random joke from the API.

import { useState, useEffect } from 'react'

function useFetch(url) {
  const [data, setData] = useState(null)

  useEffect(() => {
    fetch(url)
      .then((res) => res.json())
      .then((data) => setData(data))
  }, [url])

  return [data]
}

export default useFetch
useFetch.js

We created a new file and named it useFetch.js which contains our custom logic to fetch data from the API endpoint.

In App.js, we are importing our useFetch Hook and utilize it like any other Hook. This is where we pass in the URL to fetch data.

Now we can reuse this custom Hook in any component to fetch data from any URL.

import useFetch from './components/useGetRandomJoke'
function App() {
  const [data] = useFetch('http://api.icndb.com/jokes/random')

  return <div className="App">{data && <p>{data.value.joke}</p>}</div>
}

export default App
App.js

Conclusion

This article explored built-in react hooks and their appropriate use in the React application. If you like the article then please share it and feel free to ask any questions you may have in the comment section below.