• JAMstack

React Hooks: The Fundamental Guide for Your Projects

Michael Hungbo
Michael Hungbo
December 6, 2023 · 20 min read
Microservices vs. API: Relationship and Difference Between Them

React hooks were first introduced in React 16.8.0 and have significantly changed the way developers write code in React. Hooks allow you to utilize state and other React features without the need for class components.

This article provides a comprehensive overview of React hooks, covering how to use them, their significance, and best practices for implementing them in your projects. Additionally, we'll guide you through creating your custom React hook from scratch, equipping you to leverage their capabilities in your apps immediately.

Whether you're a seasoned developer or just starting with React, this article will equip you with all the necessary knowledge required to harness the complete potential of React hooks in your projects.

What Are React Hooks?

React hooks are functional JavaScript components that enable you to isolate stateful logic in a React component, making it reusable in your app. With React hooks, we can tap into React state and lifecycle functionalities right within function components.

In early React versions, lifecycle methods were exclusive to class components. Hooks offer a simplified and more intuitive approach to handle state and lifecycle events in functional components, which were initially prescriptive to presentational and stateless functions.

What Are the Key Advantages of Using React Hooks?

When hooks were first introduced in React, the React team sought to address a few key challenges with class components.

Firstly, the team recognized that maintaining components that grow into an unmanageable mess of stateful logic and side effects makes it easy to introduce bugs and inconsistencies. They introduced hooks to help developers avoid this pitfall by isolating stateful logic in separate functions.

Secondly, the React team acknowledged that classes can be a significant learning barrier to React. Developing within classes can be frustrating for developers accustomed to other programming languages who struggle to grasp how the this keyword works in React. Moreover, class components can lead to unintentional patterns that slow down code compilation in React.

React hooks provide a solution to these challenges by providing developers with additional features and facilitating the creation of more modular and reusable components. This makes it easier to write and maintain complex applications while avoiding the issues that can arise from using class components.

How to Use React Hooks?

It is critical to keep in mind that every React hook begins with the word use, a crucial rule to consider when creating custom hooks. Keep in mind that hooks can only work in React function components and not in class components.

React provides a wide range of built-in hooks, such as useState, useEffect, useContext, useReducer, and many others.

To illustrate, consider this simple example that uses the useState hook in a function component to create a counter.

import React, {useState} from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }

Examples of React Hooks

As previously stated, React includes several default hooks in the version 16.8.0 release.

In this section, we'll delve into the essential and useful hooks you need to be aware of.

The useState Hook

The useState hook is one of the state hooks in React and also one of the most commonly used. It allows components to have access to and manipulate stateful logic in an application. In React, a state is any information in your application that changes over time, usually triggered by user interaction.

The useState hook has the following signature:

const [state, setState] = useState(initialState);

The useState hook is a function that takes an initial state (initialState) as its only parameter and then returns an array with two items: the current state value (state) and a function to update that value (setState). The initial state value can range from any data type in JavaScript, including arrays, objects, strings, and boolean.

Here’s an example showing how you’d use the useState hook to create a popup that appears on the screen when the user clicks a button:

import React, { useState } from 'react'; export default function Popup() { const [isOpen, setIsOpen] = useState(false); const togglePopup = () => { setIsOpen(!isOpen); }; return ( <div> <button onClick={togglePopup}>Click to open popup</button> {isOpen && ( <div className="popup"> <h2>I'm a popup/h2> <p>Popup Content</p> <button onClick={togglePopup}>Close</button> </div> )} </div> ); }

Here’s an explanation of what is going on in the code above.

To use any of the built-in hooks in React, we have to first import it from the react module.

The useState hooks work by returning an array with two values: the current state value and a function to update that value.

In the above example, we defined the function as follows:

const [isOpen, setIsOpen] = useState(false);

The above code snippet defines a useState function holding a boolean as its initial value. It returns two items, isOpen, which holds the current state value, and setIsOpen, which is a function to update the state value.

Next, in the code, we created a function togglePopup that runs the setIsOpen function, which changes the current state value whenever a button is clicked and causes React’s virtual DOM to re-render.

We used the useState hook in this example to manage the state of the popup, which changes after the user performs an action (clicking a button).

You can go through the React docs here to learn more about the useState hook.

The useEffect Hook

The useEffect hook is a built-in hook in React that allows components to synchronize with external systems in a functional way. External systems (also called side effects) include browser APIs, third-party widgets, networks, and so on.

The useEffect hook can be declared as follows:

useEffect(setup, dependencies?)

It’s a function that takes two parameters: setup, which is a function, and an optional array of dependencies.

The setup function contains your Effect’s logic and is executed after the component is mounted or updated. The optional dependencies are an array containing all reactive values referenced inside of the setup function.

The dependencies array is used to control when the effect should be re-run. If any of the values in the array changes, the component where the hook is used will re-render, and the useEffect function will re-run. If you don’t add the dependencies array to the hook, your Effect function will re-run after every re-render of the component where it's used.

Additionally, if you specify an empty array for dependencies, the Effect function will run only once at the initial render but never in subsequent re-renders.

Here’s an example showing how to use the useEffect hook to fetch data from an external API:

import React, { useState, useEffect } from 'react'; export default function FetchAPIData() { const [data, setData] = useState([]); useEffect(() => { const fetchData = async () => { const response = await fetch('https://some-api.com'); const APIData = await response.json(); setData(APIData); }; fetchData(); }, []); return ( <> <h1>API Data</h1> <ul> {data.map((item) => ( <li key={item.id}>{item.title}</li> ))} </ul> </> ); }

In the above example, we’re running a function fetchData in the useEffect hook to fetch data from a remote API somewhere (a good example of an external system). Then we store the data from the API in a data variable using the useState hook. We also passed an empty dependency array to useEffect, which means the hook will only run once when the component is first mounted.

The useReducer Hook

Like useState, the useReducer hook is another built-in hook for managing stateful logic in your React applications. It’s often used as an alternative to useState to manage a complex state or when the next state depends on the previous one.

The useReducer hooks manage the state in your React application by letting you use a reducer (a function that takes in the current state and an action object and returns a new state based on the action) in your components.

The signature for the useReducer hook is as follows:

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

The useReducer hook takes two required parameters, reducer and initialArg, and one optional parameter, init. The reducer is a function that specifies how the state gets updated. initialArg is the value from which the initial state is calculated, while init is used as an initializer function to create the initial state if it’s not already created.

useReducer returns two values: state, which is the current state, and dispatch, a function for updating the state to a different value (also causing a re-render).

Below is an example showing the useReducer how can we use the useReducer hook to manage a list of users in an array, with the functionality to create and delete a user.

import React, { useReducer } from 'react'; // Define initial state const initialState = { users: [{id: 123, name: "Tom Jerry", age: 19}], }; // Define reducer function const userReducer = (state, action) => { switch (action.type) { case 'CREATE_USER': return { ...state, users: [...state.users, action.payload] }; case 'DELETE_USER': return { ...state, users: state.users.filter(user => user.id !== action.payload) }; default: return state; } }; const UserList = () => { const [state, dispatch] = useReducer(userReducer, initialState); // Handler for creating a new user const handleCreateUser = (user) => { dispatch({ type: 'CREATE_USER', payload: user }); }; // Handler for deleting a user const handleDeleteUser = (userId) => { dispatch({ type: 'DELETE_USER', payload: userId }); }; return ( <div> <h2>User List</h2> <ul> {state.users.map(user => ( <li key={user.id}> {user.name} <button onClick={() => handleDeleteUser(user.id)}>Delete</button> </li> ))} </ul> <h2>Add User</h2> <form onSubmit={(e) => { e.preventDefault() }}> <input type="text" placeholder="Name" /> <input type="number" placeholder="Age" /> <button onClick={(e) => handleCreateUser({ id: Date.now(), name: e.target.value, age: e.target.value })}>Create User</button> </form> </div> ); }; export default UserList;

In the example above, the initialState represents the initial state of the user's array, which contains a single user. The userReducer is a function that takes the current state and an action and returns the updated state based on the action type.

Next, we use the handleCreateUser and handleDeleteUserfunctions as event handlers to dispatch actions to the reducer to update the state. The UserList component then renders the list of all users, each with delete functionality, and finally, a form to add new users to the list.

The useContext Hook

useContext is one of the built-in hooks in React that allow you to interact with your application state. Specifically, useContext allows you to read and subscribe to the Context API to pass data around in your components.

If you’re familiar with React, then you already know data can only be passed in a unidirectional way - from the parent component to child components via props.

However, when you use this pattern for passing data from a parent component through multiple nested child components to a deeply nested child component that actually needs the data, it won’t be long before you find yourself in a terrible situation known as “props drilling.”

The major problem the Context API and useContext solve is to provide an alternative way for passing data through multiple and deep levels in the component tree without letting you go through props drilling.

The useContext hook provides a way to interact with the Context API to make the data available in any part of your application.

The signature for the useContext hook is as follows:

const value = useContext(SomeContext)

useContext accepts only one parameter: SomeContext. Before the useContext hook can be used, you'd need to have created the SomeContext object somewhere higher in the component tree. This context object represents the kind of information you can provide or read from components that need the data.

This great example below from the React docs shows how you can use the useContext hook to pass theme data between the main parent component App.js, to child components, Button, and Panel, without using props.

import { createContext, useContext } from 'react'; const ThemeContext = createContext(null); export default function MyApp() { return ( <ThemeContext.Provider value="dark"> <Form /> </ThemeContext.Provider> ) } function Form() { return ( <Panel title="Welcome"> <Button>Sign up</Button> <Button>Log in</Button> </Panel> ); } function Panel({ title, children }) { const theme = useContext(ThemeContext); const className = 'panel-' + theme; return ( <section className={className}> <h1>{title}</h1> {children} </section> ) } function Button({ children }) { const theme = useContext(ThemeContext); const className = 'button-' + theme; return ( <button className={className}> {children} </button> ); }

To use the Context API in React, we first need to initialize the context with a value using the createContext function from React.

In the above example, the MyApp component is the higher-level context provider that creates the ThemeContext context and makes it available to its child components using a </ThemeContext.Provider> component. The child components (Panel and Button) that need access to the data provided by the context provider can then use the useContext hook from React to access the data directly without having to pass it through props.

By using the useContext hook with the Context API to pass data around in a React application, we can avoid unnecessary props drilling in the component tree and write more efficient and intuitive React code.

The useCallback Hook

The useCallback hook is one of React’s built-in hooks for improving your application performance. Its function is to cache expensive function calls between re-renders.

Its signature looks like so:

const cachedFn = useCallback(fn, dependencies)

The two parameters for useCallback are:

  • fn: The function you want to cache between re-renders

  • dependencies: An array of all the reactive that is referenced in fn

The return value of useCallback (cachedFn in this case) is a cached version of fn that only changes if one of the dependencies has changed.

The following example from the React docs shows how you can use the useCallback hook to cache a function and prevent the component where it’s used from re-rendering.

function ProductPage({ productId, referrer, theme }) { // Tell React to cache your function between re-renders... const handleSubmit = useCallback((orderDetails) => { post('/product/' + productId + '/buy', { referrer, orderDetails, }); }, [productId, referrer]); // ...so as long as these dependencies don't change... return ( <div className={theme}> {/* ...ShippingForm will receive the same props and can skip re-rendering */} <ShippingForm onSubmit={handleSubmit} /> </div> ); }

In the above example, we’re passing a handleSubmit function down from the ProductPageto the ShippingForm component. Remember, in React, when a component re-renders, React re-renders all of its children recursively. This means when ProductPagere-renders with a different theme, the ShippingForm component also re-renders.

So let’s say, for some reason, the ShippingForm component is very slow to render. If the ShippingForm is being re-rendered every time the ProductPage re-renders, this can hurt the performance of our app significantly.

To avoid slowing down our app, we wrapped the handleSubmit function in a useCallback hook. This ensures that the function is the same between the re-renders until any of the dependencies change. More importantly, it ensures the ShippingForm component skips re-rendering.

You’ll often find useCallback and useMemo used in improving performance in React apps. However, there’s an important difference between the two optimization hooks that you should take note of.

The difference is in what each hook caches exactly. useCallback will not call the function you provided as an argument, but catches the function itself. useMemo, on the other hand, will run the function you passed as an argument and cache its result instead.

The useId Hook

The useId hook is also a built-in React hook and is used for generating unique IDs on the client-side and server side.

Its signature is as follows:

const generateID = useId()

The useId hook accepts no parameters but returns a unique string every time it’s called.

A good use case for the useId hook is generating unique IDs for HTML accessibility attributes. It’s useful for connecting two HTML elements to each other.

Take for example the following input element, and a label element with an htmlFor attribute describing the value of the input element.

import { useId } from 'react'; function Input() { const emailID = useId(); return ( <> <div> <label htmlFor={emailID}>Email</label> <input type="email" id={emailID} name="email" /> </div> </> ); }

So, instead of hardcoding the I'd value of the input, which is considered a bad practice in React, we used the useId hook to generate a unique ID that can be referenced by both the input and label elements.

You might be tempted to use the useId hook to generate keys when rendering list items in your React app. This is bad practice and it also poses some serious performance pitfalls for your app.

If you need to generate unique keys for a list, consider using a library such as uuid instead.

The useLayoutEffect Hook

useLayoutEffect is a built-in hook in React that works similarly to the useEffect hook.

useLayoutEffect is commonly used as an alternative to useEffect. However, useLayoutEffect differs in how it executes its side effect code - it runs synchronously immediately after React has performed all DOM mutations.

Its signature is also similar to useEffect's:

useLayoutEffect(setup, dependencies?)

The useLayoutEffect hook accepts two parameters; setup, which contains your Effect logic, and an optional dependencies array. Its return value is undefined.

The most common use case for useLayoutEffect is measuring the layout of a page. For example, getting the height and width of a DOM element.

Let’s take a look at an example.

import { useLayoutEffect, useRef, useState } from 'react'; export default function LayoutSize () { const divRef = useRef(null); const [elementSize, setElementSize] = useState({ width: 0, height: 0 }); useLayoutEffect(() => { const { current } = divRef; if (current) { const rect = current.getBoundingClientRect(); setElementSize({ width: rect.width, height: rect.height }); } }, []) return ( <> <div ref={divRef}> <p>Width: {elementSize.width}px</p> <p>Height: {elementSize.height}px</p> </div> </> ); };

In the example above, we're using useLayoutEffect to measure the width and height of a DOM element (a div in this case) using the getBoundingClientRect method, and updating the state with the measured values. We also passed an empty dependencies array as the second argument to useLayoutEffect. This means the effect will only run once, after the initial render.

It's important to note that because useLayoutEffect can block the browser's layout and paint process, you should use it sparingly and only when necessary. And in most cases, you’ll find useEffect sufficient for performing side effects in functional components.

The useMemo Hook

The useMemo hook is another one of React’s built-in hooks for improving performance in your application. It works by letting you memoize expensive function calls so that you can avoid calling them on every render.

Wikipedia defines memoization as follows:

Memoization or 'memoisation' is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.

The useMemo hook can be used as follows:

const cachedValue = useMemo(calculateValue, dependencies)

As you can see above, the hooks accepts two parameters; calculateValue and dependencies. It returns a single value, which is the cached result from the expensive function.

calculateValue is the expensive function which you want to cache. This function must be pure, have no argument and can return any type of value. calculateValue will be run during the initial component render. During subsequent re-renders of the component, React will return the same value again if dependencies hasn’t changed, otherwise, it’ll run the calculateValue function again, return its return and store it.

dependencies contains the list of changing values that are used in the calculateValue function. The changing values could be state, props, or variables defined in your component.

Let’s look at an example. Imagine you have a function in your component that sorts about 1000 blog posts before displaying them in a list. Running this type of function during every re-render of your component is bad and and will significantly lower the performance of your app.

Here’s how we can fix it with useMemo:

import { useMemo } from 'react'; // An expensive function to sort 1000 blog posts function sortBlogPosts(posts) { return posts.sort((a, b) => new Date(b.date) - new Date(a.date)); } export default function BlogPostList({ posts }) { const sortedBlogPosts = useMemo(() => sortBlogPosts(posts), [posts]); return ( <ul> {sortedBlogPosts.map(post => ( <li key={post.id}> <h2>{post.title}</h2> <p>{post.content}</p> </li> ))} </ul> ); }

By using useMemo in the above example, we memoize the function for sorting the array of blog posts and only recalculate it when the postsprop changes. This avoids unnecessary re-renders of the component and improves your app performance.

The basic example above shows how we can optimize our React applications for performance without going through too much hassle. However, beware of using the useMemo hook at every opportunity to optimize your application.

If you find out your app is slow, try to figure out the cause and solve the problem before using useMemo for optimization.

The useRef Hook

The useRef hook is a built-in React hook that allows you to create a mutable reference to an element or value that does not update across renders. In other words, it allows you to reference a value that’s not needed for rendering.

Aside from the above use case, it is also the most go-to hook for accessing and manipulating the DOM in React.

Its signature is as follows:

const ref = useRef(initialValue)

useRef accepts only one parameter:

  • initialValue: This is value you want the ref object’s current property to be initially. It can be a value of any type, such as a string, boolean, integer, and so on.

It returns an object with a single property, current. curent's value is initially set to the initialValue you passed to useRef, but you can change it to something else.

The example below shows how you can use the hook to automatically focus an input element after the click of a button:

import { useRef } from 'react'; export default function Form() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); } return ( <> <input ref={inputRef} /> <button onClick={handleClick}> Focus the input </button> </> ); }

In the example above, we created a reference to the input element using useRefand assign it to the input element using the ref prop. We then use inputRef.currentto access the input element’s DOM node directly in our handleClick function and call its focus method to bring the input into focus.

The useTransition Hook

useTransition is a built-in React hook for optimizing your app’s UI performance. This hook is useful if you’re transitioning your screen to render a component that makes expensive state updates. For example, trying to render a list containing hundred of items.

While there are popular solutions for deferring heavy state updates that could make your app unresponsive, such as using Suspense or loading spinners, useTransition skips any of these processes and make sure we have some content available on the next screen before we transition to it.

Basically, the useTransition slows down page transition to ensure data or content is available on the next screen (which is great for your users’ experience).

The signature for the useTransition hook is as follows:

const [isPending, startTransition] = useTransition()

The useTransition hook doesn’t take any parameters, but returns these two values:

  • isPending: a boolean value used by React to tell us whether there’s an ongoing transition.

  • startTransition: a callback function for marking slow state updates as transitions.

Let’s go through an example to better understand how the hook works.

import React, { useState, useTransition } from 'react'; export default function App() { const [tab, setTab] = useState('home'); const [list, setList] = useState([]); const [isPending, startTransition] = useTransition(); const fetchLargeDataFromAPI = () => { // fetch data from a remote API // ... setList(data) } const switchTab = (nextTab) => { startTransition(() => { setTab(nextTab); }) }; return ( <> <HomeTabButton onclick={() => switchTab('home')}> Home </HomeTabButton> <ListTabButton onclick={() => switchTab('list')}> Go to list </ListTabButton> <hr/> {tab === 'home' && <HomeTab/>} {tab === 'list' && <ListTab list={list}/>} </> ); } const HomeTab = () => { return ( <p>Welcome to the home page!</p> ); } const HomeTabButton = ({ onClick, children }) => { return ( <button onClick={() => { onClick(); }}> {children} </button> ) } const ListTab = ({list}) => { return ( <ul> {list.map((item) => ( <li key={item.id}>{item.text}</li> ))} </ul> ); } const ListTabButton = ({ onClick, children }) => { return ( <button onClick={() => { onClick(); }}> {children} </button> ) }

In the example above, we’re using the useStatehook to create two state variables, listand isPending. list is initialized as an empty array, and will be updated with the list of items fetched from the API. isPending is a boolean flag that is used to indicate whether the transition is in progress.

The ListTabButton component is a reusable button component that accepts an onClick callback function and children as props. The useTransition hook is used to create a startTransition function, which is used to initiate the transition.

When the button is clicked, the onClick callback function is executed inside a startTransition function. This signals to React that a transition is about to start, and React will use this information to optimize the rendering of the component.

How to Build a Custom React Hook?

In this section, we’ll build a custom React hook from scratch. Our hook, which we’ll call useMediaQuery, is a hook that allows you to detect the current media query that the user's device matches. This can be useful if you want to allow some specific features to work or not on your website, depending on the type of device the user is accessing your site with.

Before we start building out our custom hook, there are some hooks rules in React you should keep in my mind. And if you violate any of these rules, there’s a linter plugin in React you can install that will raise errors or warn you to ensure you adhere to these rules.

Here are the important rules to remember:

  • Your custom hook name must start with the use keyword.

  • You can only call hooks at the top level. Meaning you can’t use hooks in functions, inside another hook, loops, or nested functions. However, you can use React built-in hooks in a custom hook.

  • Only call hooks from React functions (function components and custom hooks).

Now let’s get back to building our custom hook.

Create a file useMediaQuery.js with the following content:

import { useState, useEffect } from 'react'; export default function useMediaQuery(query){ const [matches, setMatches] = useState(false); useEffect(() => { const media = window.matchMedia(query); if (media.matches !== matches) { setMatches(media.matches); } }, [query]); return matches; };

Basically, our custom hook, useMediaQuery, takes a single parameter, which is the media query to match against. The hook then returns true or false, indicating whether the device currently matches the given media query.

Let’s break down the above code even further.

  1. We start by importing the useState and useEffect hooks from the react library.

  2. Inside the useMediaQuery function (which is our hook function), we declare a state variable called matches using the useState hook. This variable will hold the Boolean value indicating whether the device matches the specified media query.

  3. We then use the useEffect hook to set up a listener for changes in the device's media query. We also pass in the query argument as a dependency, this will make the hook to re-run whenever the query changes.

  4. Inside the useEffect hook, we create a new MediaQueryList object using the window.matchMedia method, passing in the query argument. Next, we check if the

    matches property of this object matches the current value of matches in our state. If it doesn't, we update the state to reflect the new value.

  5. Finally, we return the matches state variable, which holds the Boolean value indicating whether the device currently matches the specified media query.

And here’s how we’d use our hook in a React component:

import React from 'react'; import useMediaQuery from './useMediaQuery.js'; export default function LandingPage() { const isDesktop = useMediaQuery('(min-width: 768px)'); return ( <div> {isDesktop ? ( <p>This is a desktop device.</p> ) : ( <p>This is not a desktop device.</p> )} </div> ); };

This is just to give you an idea of what is achievable with hooks in React. There’s no limit to what logic you can extract into a hook. Ensure you’re not breaking the rules!

You can also check out usehooks.com to see more examples and get ideas of custom React hooks you can use in your next project.

The Bottom Line

Hooks are a powerful addition and game-changers to React. They allow you to isolate and reuse similar functionalities across your apps.

With React Hooks, developers can write intuitive React code and demystify complex applications.

In this article, we walked you through the ins and outs of React Hooks. You learned why Hooks were introduced into React, what problems they solve, and how you can start using them in your projects. And more importantly, you are now a rock star in creating your own custom React hooks.

Best JavaScript Frameworks for Your Next Project

Download the best JavaScript frameworks guide to answer the 'which JavaScript framework to use for your next project?'

Keep Reading on This Topic
Headless CMS for Personalization
Blog Posts
You Need a Headless CMS for the True Personalization

Here, in this post, we'll take a deep dive into why you need a headless CMS for meaningful personalization, how headless CMS personalization works, and how to personalize websites that use headless CMS.

Personalization Maturity Model
Blog Posts
Personalization Maturity Model: When and How Should You Personalize Customer Experience

Given the constant need for customers to be recognized as being unique, it has now become more complex to understand or segment them.