Unlocking the Power of Custom Hooks in React: A Beginner’s Guide to Building Reusable and Efficient Components

Oladipupo Ishola
9 min readMay 20, 2023

Are you tired of writing the same code in your React components over and over? Do you want to advance your skills and write more modular, efficient, and reusable code? Then it's time to learn about React's custom hooks!

In this beginner-friendly guide, we'll delve into the world of custom hooks and show you how to make your own hooks from scratch. Part 1 of this article will begin with simple examples that illustrate basic concepts, and Part 2 will gradually progress to more advanced use cases.

You'll learn how to turn complex logic into reusable functions, which will make your code more modular and easier to test. You'll also learn how to abstract common patterns and behaviors into custom hooks, making your React components easier to maintain and update.

This guide will provide you with the tools and knowledge you need to unlock the full power of custom hooks in React, whether you're a seasoned React developer looking to improve your skills or a beginner just getting started. So let's get started and create some amazing components!

What, then, is React Hook?

React Hooks were introduced in React version 16.8, which was released in February 2019. Prior to Hooks, stateful logic in React components was managed using class components and lifecycle methods. Hooks provide a way to use stateful logic and other React features in functional components, which simplifies the code and makes it more reusable.

The following are some of the benefits of using React Hooks:

  1. Improved code readability: Hooks allow you to extract complex logic into reusable functions, making your code easier to read and understand.
  2. Simplified state management: With Hooks, you can manage the state of functional components without the need for classes and lifecycle methods.
  3. Reuseability: Hooks make it easier to reuse logic across multiple components, which reduces code duplication and improves code maintainability.
  4. Better performance: Hooks can improve performance by reducing the number of re-renders and improving the efficiency of state updates.
  5. Simplified testing: Since Hooks are just functions, they can be easily tested with standard JavaScript testing tools like Jest.
  6. Overall, React Hooks have revolutionized the way developers approach stateful logic in React applications, making it simpler, more reusable, and easier to maintain.

Basic Concepts of Custom Hooks

Custom hooks are simply functions defined and used in React components to encapsulate common patterns or behaviors. Custom hooks allow you to extract logic that would otherwise be repeated across multiple components, making your code more modular and easier to maintain.

Understanding custom hooks requires an understanding of two related React concepts: state and lifecycle methods.

State allows React components to manage and update their own data. When the state of a component changes, React automatically re-renders the component and any child components that rely on it.

Lifecycle methods are methods defined in a React component that are called at specific points in the component's life cycle. The componentDidMount method, for example, is called when a component is mounted (i.e., added to the DOM), whereas the componentDidUpdate method is called when a component's state or props change.

Custom hooks extend these ideas by allowing you to encapsulate common state and lifecycle logic in reusable functions. You could, for example, write a custom hook that manages the state of a form input or one that handles the logic for showing and hiding a modal dialog.

You can use the built-in React hooks (such as useState and useEffect) to manage state and lifecycle methods when you define a custom hook, just like you would in a regular component. The difference now is that your custom hook is a reusable function that can be called from any component that requires that functionality.

In the following section, we'll go over an example of a basic custom hook for form input management, so you can see how these ideas work in practice.

Building Your First Custom Hook

Assume you have a form with multiple inputs that you want to manage the state of in a reusable manner. You could write a custom hook that manages each input's state and returns the current value, as well as a function to update the value.

Here's an example of how that custom hook could look:

import { useState } from 'react';

function useInput(initialValue) {
const [value, setValue] = useState(initialValue);

function handleChange(event) {
setValue(event.target.value);
}

return [value, handleChange];
}

The useState hook is used in this custom hook to manage the state of the input value. We also define a handleChange function that updates the state whenever the input value changes.

Finally, we return an array that contains the current value and the handleChange function. This way, we can easily destructure the array in our component and use the current value and handleChange function for each input.

Here’s an example of how you might use this custom hook in a form component:

import React from 'react';
import useInput from './useInput';

function Form() {
const [firstName, handleFirstNameChange] = useInput('');
const [lastName, handleLastNameChange] = useInput('');

function handleSubmit(event) {
event.preventDefault();
console.log('First Name:', firstName);
console.log('Last Name:', lastName);
}

return (
<form onSubmit={handleSubmit}>
<label>
First Name:
<input type="text" value={firstName} onChange={handleFirstNameChange} />
</label>
<label>
Last Name:
<input type="text" value={lastName} onChange={handleLastNameChange} />
</label>
<button type="submit">Submit</button>
</form>
);
}

The useInput custom hook is used in this example to manage the state of the first and last name inputs. When the form is submitted, we also define a handleSubmit function that logs the current values of those inputs.

We've simplified and made our form component more reusable by using this custom hook. We can now easily reuse the same input state management logic in other components without repeating the code.

Custom hooks can be used for a variety of purposes other than managing form inputs.

Here are some additional examples:

  1. useLocalStorage

This custom hook lets you save and retrieve data from local storage in a way that adheres to React's state model. Here's an example of how to do it:

import { useState, useEffect } from 'react';

function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const storedValue = localStorage.getItem(key);
return storedValue !== null ? JSON.parse(storedValue) : initialValue;
});

useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);

return [value, setValue];
}

We use useState to manage the value's state and useEffect to synchronize the value with local storage whenever it changes in this custom hook. This allows us to easily save and retrieve data from local storage while adhering to React's state model.

Usage:

import { useState } from 'react';
import useLocalStorage from './useLocalStorage';

function ExampleComponent() {
const [name, setName] = useLocalStorage('name', '');

return (
<div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<p>Hello, {name}!</p>
</div>
);
}

In this case, useLocalStorage is used to save the user's name. Thename state is initialized with the value from local storage using the useLocalStorage hook, and the input's value and setName function is used to update the stored name.

2. useToggle

This custom hook, which allows you to toggle a boolean value between true and false, is one of my favorites.

Usage:

import { useState } from 'react';

function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);

function toggle() {
setValue(!value);
}

return [value, toggle];
}

We use useState to manage the value's state in this custom hook, and we define a toggle function that updates the value to its opposite. Instead of calling useState every now and then, this can be called anywhere in the application where you need to toggle between true and false.

Usage:

import { useState } from 'react';
import useToggle from './useToggle';

function ExampleComponent() {
const [isOn, toggle] = useToggle(false);

return (
<div>
<button onClick={toggle}>{isOn ? 'ON' : 'OFF'}</button>
<p>Toggle state: {isOn ? 'ON' : 'OFF'}</p>
</div>
);
}

In this example, it is used to toggle the state of a button. The isOn state and the toggle function are provided by the useToggle hook. When you click the button, the toggle function is called, and the boolean state is updated accordingly.

3. useWindowWidth

This custom hook allows you to track the width of the window and update the rendering of your component accordingly. Here's an example of how to do it:

import { useState, useEffect } from 'react';

function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);

useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}

window.addEventListener('resize', handleResize);

return () => {
window.removeEventListener('resize', handleResize);
};
}, []);

return width;
}

We use useState to manage the state of the "width" and useEffect to update the width whenever the window is resized in this custom hook. We also add resize event listeners and remove them when the component unmounts.

Usage:

import { useEffect } from 'react';
import useWindowWidth from './useWindowWidth';

function ExampleComponent() {
const windowWidth = useWindowWidth();

useEffect(() => {
console.log('Window width:', windowWidth);
}, [windowWidth]);

return <p>Current window width: {windowWidth}px</p>;
}

In this case, it is used to log the window width to the console whenever it changes. The windowWidth state returned by the useWindowWidth hook is accessed in the useEffect hook's dependency array to trigger the effect whenever the window width updates.

4. useDocumentTitle

This custom hook allows you to dynamically update the document title based on a provided value. It uses the useEffect hook to update the title whenever the title prop changes.

import { useEffect } from 'react';

function useDocumentTitle(title) {
useEffect(() => {
document.title = title;
}, [title]);
}

Usage:

import { useEffect } from 'react';
import useDocumentTitle from './useDocumentTitle';

function ExampleComponent() {
useDocumentTitle('New Page Title');

return <p>This is the content of the component.</p>;
}

In this example, the useDocumentTitle is used to update the title of the page to "New Page Title" when the component mounts.

5. useKeyPress

The useKeyPress hook allows you to detect whether a specific key is currently being pressed. It sets the state isKeyPressed to true when the target key is pressed and false when it is released. It uses the useEffect hook to add and remove event listeners for the 'keydown' and 'keyup' events on the document.

import { useState, useEffect } from 'react';

function useKeyPress(targetKey) {
const [isKeyPressed, setIsKeyPressed] = useState(false);

useEffect(() => {
function handleKeyDown(event) {
if (event.key === targetKey) {
setIsKeyPressed(true);
}
}

function handleKeyUp(event) {
if (event.key === targetKey) {
setIsKeyPressed(false);
}
}

document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keyup', handleKeyUp);

return () => {
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('keyup', handleKeyUp);
};
}, [targetKey]);

return isKeyPressed;
}

Usage:

import { useState, useEffect } from 'react';
import useKeyPress from './useKeyPress';

function ExampleComponent() {
const [isEnterPressed, setIsEnterPressed] = useState(false);

useKeyPress('Enter', () => setIsEnterPressed(true));

return (
<div>
<p>Press Enter key: {isEnterPressed ? 'Yes' : 'No'}</p>
</div>
);
}

In this example, it is used to determine if the Enter key is pressed. The isEnterPressed state is updated to true when the Enter key is pressed.

6. useScrollPosition

The useScrollPosition hook allows you to keep track of the current scroll position of the window. It initializes the state with the initial scroll position and updates it whenever the user scrolls. The useEffect hook is used to add and remove the scroll event listener.

import { useState, useEffect } from 'react';

function useScrollPosition() {
const [scrollPosition, setScrollPosition] = useState(0);

useEffect(() => {
function handleScroll() {
const position = window.scrollY || window.pageYOffset;
setScrollPosition(position);
}

window.addEventListener('scroll', handleScroll);

return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);

return scrollPosition;
}

Usage:

import { useEffect } from 'react';
import useScrollPosition from './useScrollPosition';

function ExampleComponent() {
const scrollPosition = useScrollPosition();

useEffect(() => {
console.log('Scroll position:', scrollPosition);
}, [scrollPosition]);

return <p>Current scroll position: {scrollPosition}px</p>;
}

In this example, it is used to log the scroll position to the console whenever it changes. The scrollPosition state returned by the useScrollPosition hook is accessed in the useEffect hook's dependency array to trigger the effect whenever the scroll position updates.

7. useClickOutside

The useClickOutside hook allows you to detect clicks that occur outside of a specified element. It takes a handler function as an argument, which will be executed when a click outside the element occurs. It uses the useRef hook to create a reference to the element and the useEffect hook to add and remove the click event listener on the document.

import { useEffect, useRef } from 'react';

function useClickOutside(handler) {
const ref = useRef(null);

useEffect(() => {
function handleClickOutside(event) {
if (ref.current && !ref.current.contains(event.target)) {
handler();
}
}

document.addEventListener('click', handleClickOutside);

return () => {
document.removeEventListener('click', handleClickOutside);
};
}, [handler]);

return ref;
}

Usage:

import { useRef, useEffect } from 'react';
import useClickOutside from './useClickOutside';

function ExampleComponent() {
const ref = useRef(null);

useClickOutside(ref, handleClickOutside);

function handleClickOutside() {
console.log('Clicked outside the element.');
}

return (
<div ref={ref}>
<p>Click outside this element to trigger the callback.</p>
</div>
);
}

In this example, the ref is used to reference the outer element. When a click happens outside the element, the handleClickOutside function is called, logging a message to the console.

These are just a few basic custom hooks to get you started; in the second part of this article, we’ll explore more advanced custom hook examples and how you can use them to encapsulate even more complex logic.

Thank you for reading!

--

--

Oladipupo Ishola

Tech Writer | Mentor & Code Instructor | Bootcamp Grad | Full Stack Developer | Frontend Developer | Backend Developer | MLH ’21 Alum