liveBook · Manning

Custom hook

As explained in React in Depth

A custom hook is a JavaScript function that follows React hook conventions to encapsulate reusable stateful or effectful component logic.

A custom hook moves React logic out of a component while giving the component a focused API. It can encapsulate state and effects, wrap context access, or expose data-layer operations. The useful boundary is not simply fewer lines: it is a component whose inputs, returned values, and JSX make its purpose easier to see. section 2.3 section 8.2

Extract one component’s state

A radio group needs a selected value, a change handler, and a value for its context provider. Move that work into useContextValue({ name, onChange }). The hook calls useState, updates the selection, invokes the supplied onChange callback when present, and returns { name, selectedValue, onChange: handleChange }. RadioGroup can then obtain its provider value in one call and concentrate on rendering. This is a small extraction, not a new state-management system. section 2.3.1

For a larger component, hooks can compose. A user-profile hook calls a separate data-fetching hook and manages edit mode and preferences; the component receives the data and actions it needs to render view or edit mode. The component file becomes shorter, although the total code grows by another file. Prefer the split when those smaller files have clear responsibilities, rather than treating line count alone as the goal. section 2.3.2

Listing 2.16 User profile custom hook
import { useEffect, useState } from "react";
import { useAPI } from "./useAPI";
function useFetchUserData(userId) {     #1
  const [userData, setUserData] = useState(null);
  const api = useAPI();
  useEffect(() => {
    // API call to fetch user data
    api
      .fetchUser(userId)
      .then((response) => response.json())
      .then((data) => setUserData(data));
  }, [api, userId]);
  return userData;
}
export function useUserProfile(userId) {    #2
  const [editMode, setEditMode] = useState(false);
  const [userPreferences, setUserPreferences] = useState({
    theme: "light",
    notifications: true,
  });
  const userData = useFetchUserData(userId);
  useEffect(() => {
    const storedPreferences = localStorage.getItem("userPreferences");
    if (storedPreferences) {
      setUserPreferences(JSON.parse(storedPreferences));
    }
  }, []);
  useEffect(() => {
    localStorage.setItem(
      "userPreferences",
      JSON.stringify(userPreferences)
    );
  }, [userPreferences]);
  const toggleEditMode = () => setEditMode(!editMode);
  const updatePreferences = (newPreferences) =>
    setUserPreferences(newPreferences);
  return {     #3
    userData,    #3
    editMode,   #3
    userPreferences,    #3
    toggleEditMode,   #3
    updatePreferences,  #3
  };   
}

The user-profile implementation shows a custom hook calling another hook while collecting state and effects behind one component-facing API. section 2.3.2

Choose the API to match the boundary. A context-access hook can replace repeated useContext calls, while a dedicated provider owns the corresponding state. At an application boundary, separate hooks can expose the current selection, a collection, an add operation, or an item with its available actions. In both cases, callers depend on a purposeful interface rather than handling every underlying operation directly. section 2.1.2 section 8.2

Test a shared hook directly when its input combinations would be awkward to cover through every consuming component. Hooks cannot be tested by calling them as ordinary functions. React Testing Library’s renderHook supplies a current result; a keyboard-state test checks that an unrelated key leaves the result false, the target key changes it to true, and releasing that key returns it to false. section 10.2.4

Listing 10.13 Testing a custom hook
import { renderHook } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useIsKeyPressed } from "./useIsKeyPressed";
test(
  "useIsKeyPressed should react to the target key and only that key",
  async () => {
  // ARRANGE
    const { result } =    #1
      renderHook(() => useIsKeyPressed("h"));    #1
  const user = userEvent.setup();   
  // ASSERT
  expect(result.current).toBe(false);     #2
  // ACT
  await user.keyboard("{f>}");     #3
  // ASSERT    #3
  expect(result.current).toBe(false);   #3
  // ACT
  await user.keyboard("{h>}");    #4
  // ASSERT  #4
  expect(result.current).toBe(true);    #4
  // ACT
  await user.keyboard("{/f}");     #5
  // ASSERT    #5
  expect(result.current).toBe(true);    #5
  // ACT
  await user.keyboard("{/h}");     #6
  // ASSERT    #6
  expect(result.current).toBe(false);   #6
});

The test shows renderHook, user-event keyboard actions, and assertions against the hook’s changing return value. section 10.2.4

How React in Depth covers it

How deep it goes
Works through single-hook and composed-hook extractions, application-facing APIs, and a direct test rather than hook internals.
Examples
Uses a radio group, user profile, application data API, and keyboard-state test as concrete examples.
What it assumes
Assumes familiarity with React components, props, state, effects, and context rather than teaching those APIs from zero.
What you can do after
Build it from scratch by extracting focused component logic, then apply React Testing Library’s renderHook to test a shared hook.
How it connects
Connects custom hooks to React context, providers, state, effects, data layers, and unit testing.

Also explained in

Compare how 4 books explain custom hook →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of React in Depth. The section links point to the parts of the book it draws on.