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
anduseLocalStorage
.
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 fromReact.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 touseState
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?
- 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
anduseEffect
logic across different components, that’s a prime candidate for a custom Hook. - 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.
- Testability: Logic extracted into a custom Hook can often be tested in isolation, which can be simpler than testing a component that uses it.
- 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:
-
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
anduseEffect
calls.
-
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:
- Identifying Reusable Logic: Look for patterns of
useState
,useEffect
, or other Hook calls that you find yourself repeating across multiple components. - 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.
- Define a JavaScript function whose name starts with
- 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.
- 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:
import React, { useState, useEffect } from 'react';
function ComponentA() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
// ...
}
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:
import { useEffect } from 'react';
function useDocumentTitle(title: string) {
useEffect(() => {
document.title = title;
}, [title]); // Re-run effect if title changes
}
export default useDocumentTitle;
import { useState } from 'react';
import useDocumentTitle from './useDocumentTitle';
function ComponentA() {
const [count, setCount] = useState(0);
useDocumentTitle(`Count: ${count}`); // Use the custom Hook!
// ...
}
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
andB
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 (likeuseEffect
oruseState
) 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:
- The Hook should return an object like
{ width: number, height: number }
. - It should use
useState
to store the width and height. - It should use
useEffect
to add an event listener for theresize
event on thewindow
object when the Hook is first used (component mounts). - The event listener should update the width and height state.
- The
useEffect
should clean up the event listener when the component using the Hook unmounts. - 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:
- The Hook should accept two arguments:
key
(string, for thelocalStorage
key) andinitialValue
(the initial value if nothing is inlocalStorage
for that key). - It should return an array similar to
useState
:[storedValue, setStoredValue]
. - When the Hook is first used:
- It should try to retrieve the value from
localStorage
using the providedkey
. - 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.
- It should try to retrieve the value from
- The
setStoredValue
function should:- Update the state in the component.
- Update the value in
localStorage
(serializing it to JSON).
- Consider how this Hook would behave if used by multiple components with the same key (optional, for advanced thought).
- 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
- React Docs:
Additional Resources
- React Custom Hooks Library (A curated list of useful custom hooks)
- Crafting Reusable Hooks Like a Pro