- JAMstack
React Hooks: The Fundamental Guide for Your Projects
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 handleDeleteUser
functions 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-rendersdependencies
: An array of all the reactive that is referenced infn
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 ProductPage
to the ShippingForm
component. Remember, in React, when a component re-renders, React re-renders all of its children recursively. This means when ProductPage
re-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 posts
prop 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 useRef
and assign it to the input element using the ref
prop. We then use inputRef.current
to 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 useState
hook to create two state variables, list
and 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.
We start by importing the
useState
anduseEffect
hooks from the react library.Inside the
useMediaQuery
function (which is our hook function), we declare a state variable calledmatches
using theuseState
hook. This variable will hold the Boolean value indicating whether the device matches the specified media query.We then use the
useEffect
hook to set up a listener for changes in the device's media query. We also pass in thequery
argument as a dependency, this will make the hook to re-run whenever the query changes.Inside the
useEffect
hook, we create a newMediaQueryList
object using thewindow.matchMedia
query
argument. Next, we check if thematches
property of this object matches the current value ofmatches
in our state. If it doesn't, we update the state to reflect the new value.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.
Download the best JavaScript frameworks guide to answer the 'which JavaScript framework to use for your next project?'