Skip to Content
Lesson 3

Custom Hooks

Workplace Context

As you build more complex React applications, you’ll often find yourself writing similar stateful logic in multiple components. For example, you might need to manage form input state, track window size, or interact with browser storage. Repeating this logic across components leads to code duplication, making your application harder to maintain and test. Custom Hooks provide an elegant solution to this problem, allowing you to extract and reuse stateful logic, promoting cleaner, more modular, and more maintainable codebases — a key aspect of professional React development.

Learning Objectives

By the end of this lesson, learners will be able to:

  • Explain what custom Hooks are and why they are useful.
  • Understand and apply the Rules of Hooks when creating custom Hooks.
  • Create custom Hooks to encapsulate and reuse stateful component logic.
  • Follow naming conventions for custom Hooks.
  • Describe basic approaches to testing custom Hooks.
  • Implement practical custom Hooks like useWindowSize and useLocalStorage.

Introduction to Hooks: A Quick Recap

Before diving into custom Hooks, let’s briefly revisit what Hooks are and mention some common built-in Hooks you have encountered or will encounter. Hooks are functions that let you “hook into” React state and lifecycle features from function components. They were introduced to allow you to use state and other React features without writing a class.

Common Built-in Hooks (Brief Overview for Context):

  • useState: Manages state within a functional component. You’ve used this extensively to make your components dynamic.
  • useEffect: Performs side effects in functional components, such as data fetching, subscriptions, or manually changing the DOM. You explored this in depth in Lessons 1 and 2.
  • useContext: Accepts a context object (the value returned from React.createContext) and returns the current context value for that context. This is a way to pass data through the component tree without having to pass props down manually at every level. (Covered in detail in Lesson 5).
  • useReducer: An alternative to useState for managing more complex state logic, especially when the next state depends on the previous one in intricate ways, or when state updates are triggered from multiple places. It’s often preferred for state that involves multiple sub-values or when state updates follow a pattern (like Redux reducers).
  • useCallback: Returns a memoized version of a callback function that only changes if one of its dependencies has changed. This is useful for optimizing child components that rely on reference equality to prevent unnecessary renders (e.g., when passing callbacks to optimized child components that depend on them).
  • useMemo: Returns a memoized value. It only recomputes the memoized value when one of its dependencies has changed. This is useful for optimizing expensive calculations.
  • useRef: Returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). useRef can be used to hold a reference to a DOM element or to store any mutable value that doesn’t cause a re-render when it changes.

This lesson focuses on creating your own Hooks, building upon the foundation of these built-in Hooks, but you should explore the documentation for these and other Hooks to get a better understanding of their capabilities and use cases for the future.


What are Custom Hooks?

A custom Hook is a JavaScript function whose name starts with use and that can call other Hooks.

That’s it! It’s not a special feature built into React itself, but rather a convention that follows from the design of Hooks. Custom Hooks allow you to extract component logic into reusable functions.

Why use Custom Hooks?

  1. Reusability: The primary benefit. You can write stateful logic once and use it in multiple components. If you find yourself copying and pasting the same useState and useEffect logic across different components, that’s a prime candidate for a custom Hook.
  2. Readability & Organization: Custom Hooks help keep your component code cleaner and more focused on its rendering responsibilities. Complex logic can be neatly tucked away into a custom Hook, making the component easier to understand.
  3. Testability: Logic extracted into a custom Hook can often be tested in isolation, which can be simpler than testing a component that uses it.
  4. Separation of Concerns: They help separate UI concerns (what the component renders) from logic concerns (how state is managed, how side effects are performed).

Imagine you have several components that need to track the window’s current width and height to adjust their layout. Instead of each component setting up its own useState for width/height and useEffect to listen to resize events, you could create a useWindowSize custom Hook.


Rules of Hooks (Revisited)

The same two rules that apply to built-in Hooks also apply when you’re writing custom Hooks:

  1. Only Call Hooks at the Top Level:

    • Don’t call Hooks inside loops, conditions, or nested functions.
    • Always use Hooks at the top level of your React function (either a component or another custom Hook) before any early returns.
    • Why? This ensures that Hooks are called in the same order each time a component renders, which is what allows React to correctly preserve the state of Hooks between multiple useState and useEffect calls.
  2. Only Call Hooks from React Functions:

    • Don’t call Hooks from regular JavaScript functions.
    • You can call Hooks from:
      • React function components.
      • Your own custom Hooks.
    • Why? Hooks are designed to work within the context of a React component’s lifecycle and state management.

When you create a custom Hook, you are essentially creating a new building block that can participate in this Hook ecosystem, so it must adhere to these rules. ESLint plugins (like eslint-plugin-react-hooks) will help enforce these rules in your custom Hooks too.


Creating Reusable Logic with Custom Hooks

Let’s explore how to build a custom Hook. The process generally involves:

  1. Identifying Reusable Logic: Look for patterns of useState, useEffect, or other Hook calls that you find yourself repeating across multiple components.
  2. Creating the Hook Function:
    • Define a JavaScript function whose name starts with use (e.g., useFriendStatus, useFormInput, useWindowSize).
    • Move the repeated Hook logic (the useState, useEffect calls, etc.) into this function.
  3. Defining Inputs and Outputs:
    • Inputs (Parameters): Decide what parameters your custom Hook needs to accept. These are like the props for your Hook.
    • Outputs (Return Value): Decide what your custom Hook should return. This could be a single value, an array (like useState), or an object. This is what the components using the Hook will receive.
  4. Using the Custom Hook: Call your custom Hook from your components just like you would a built-in Hook.

Example: A Simple useDocumentTitle Hook

Let’s say you have multiple components that need to set the document title.

Without a custom Hook:

ComponentA.tsx
import React, { useState, useEffect } from 'react'; function ComponentA() { const [count, setCount] = useState(0); useEffect(() => { document.title = `Count: ${count}`; }, [count]); // ... }
ComponentB.tsx
import React, { useEffect } from 'react'; function ComponentB({ userName }) { useEffect(() => { document.title = `Profile: ${userName}`; }, [userName]); // ... }

This useEffect logic for document.title is repeated. Let’s extract it.

With a useDocumentTitle custom Hook:

useDocumentTitle.ts
import { useEffect } from 'react'; function useDocumentTitle(title: string) { useEffect(() => { document.title = title; }, [title]); // Re-run effect if title changes } export default useDocumentTitle;
ComponentA.tsx
import { useState } from 'react'; import useDocumentTitle from './useDocumentTitle'; function ComponentA() { const [count, setCount] = useState(0); useDocumentTitle(`Count: ${count}`); // Use the custom Hook! // ... }
ComponentB.tsx
import useDocumentTitle from './useDocumentTitle'; function ComponentB({ userName }) { useDocumentTitle(`Profile: ${userName}`); // Use the custom Hook! // ... }

In this example:

  • We created useDocumentTitle.ts.
  • It takes title as an argument.
  • It uses useEffect to set the document title.
  • It doesn’t return anything because its only purpose is to perform a side effect.
  • Components A and B now use this Hook, making their code simpler and the title-setting logic reusable.

Naming Conventions

As mentioned, custom Hooks must start with the word use.

  • Examples: useAuth, useForm, useFetchData, useScreenOrientation.
  • Why use? This convention is critical. It allows React and tools like ESLint to automatically detect that a function is a Hook and enforce the Rules of Hooks. If you name your function like a regular JavaScript function (e.g., getDocumentTitle), React won’t know it’s a Hook, and you won’t be able to use other Hooks (like useEffect or useState) inside it, nor will ESLint check it for rule violations.

Activity 1: useWindowSize Hook

Task: Create a custom Hook named useWindowSize that tracks the current width and height of the browser window.

Requirements:

  1. The Hook should return an object like { width: number, height: number }.
  2. It should use useState to store the width and height.
  3. It should use useEffect to add an event listener for the resize event on the window object when the Hook is first used (component mounts).
  4. The event listener should update the width and height state.
  5. The useEffect should clean up the event listener when the component using the Hook unmounts.
  6. Provide an example component that uses useWindowSize to display the current window dimensions.

Activity 2: useLocalStorage Hook

Task: Create a custom Hook named useLocalStorage that allows a component to synchronize a piece of state with the browser’s localStorage.

Requirements:

  1. The Hook should accept two arguments: key (string, for the localStorage key) and initialValue (the initial value if nothing is in localStorage for that key).
  2. It should return an array similar to useState: [storedValue, setStoredValue].
  3. When the Hook is first used:
    • It should try to retrieve the value from localStorage using the provided key.
    • If a value exists in localStorage, it should be parsed (assume JSON) and used as the initial state.
    • If no value exists or if parsing fails, initialValue should be used.
  4. The setStoredValue function should:
    • Update the state in the component.
    • Update the value in localStorage (serializing it to JSON).
  5. Consider how this Hook would behave if used by multiple components with the same key (optional, for advanced thought).
  6. Provide an example component that uses useLocalStorage to store and retrieve a user’s preference (e.g., a theme name or a simple counter).

Knowledge Check

What is the primary naming convention for custom Hooks in React?

  • Select an answer to view feedback.

Which of the following best describes why the Rules of Hooks apply to custom Hooks?

  • Select an answer to view feedback.

If you want your custom Hook to provide a value that updates over time and also a function to change that value, what would its return signature most likely resemble?

  • Select an answer to view feedback.

Summary

Custom Hooks are a powerful feature in React that allow you to extract and reuse stateful logic from your components. By following the use naming convention and adhering to the Rules of Hooks, you can create your own composable building blocks. This promotes cleaner component code, reduces duplication, and makes your application logic more organized and testable. Whether it’s managing form state, interacting with browser APIs, or any other piece of stateful logic, custom Hooks are an essential tool for any React developer aiming to write maintainable and scalable applications.


References

Additional Resources