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



