Composing Multiple Hooks and Custom Hook Testing0%

Composing Multiple Hooks and Custom Hook Testing

Beginner12 min read•Updated: Jul 11, 2026
Study MaterialsCheatsheet

Composing Multiple Hooks and Custom Hook Testing

1. Composing Hooks Like Lego Bricks

One of the greatest architectural triumphs of React Hooks is composability. Just as small components can be assembled into complex layouts, small custom hooks can be combined to build powerful, specialized hooks:

Output
[useLocalStorage] + [useDebounce] ──► [useDebouncedPersistentSearch]

Consider building an auto-saving note editor:

JavaScript
// src/hooks/useAutoSaveNote.js
import { useLocalStorage } from './useLocalStorage';
import { useDebounce } from './useDebounce';
import { useEffect } from 'react';
 
export function useAutoSaveNote(noteId, initialContent = '') {
// Hook 1: Persist in localStorage
const [content, setContent] = useLocalStorage(`note_${noteId}`, initialContent);
 
// Hook 2: Debounce content for cloud sync
const debouncedContent = useDebounce(content, 1000);
 
// Effect: Sync debounced content to remote backend API
useEffect(() => {
if (debouncedContent) {
console.log(`Cloud sync note ${noteId}:`, debouncedContent);
// api.saveNote(noteId, debouncedContent);
}
}, [noteId, debouncedContent]);
 
return [content, setContent];
}

2. The Challenge of Testing Custom Hooks

You cannot test a custom hook like a standard utility function because hooks can only be executed inside a running React component lifecycle:

JavaScript
// ❌ FAILS: Cannot call hook outside a React component!
test('useLocalStorage works', () => {
const [val, setVal] = useLocalStorage('key', 'initial'); // Invalid hook call!
});

3. Testing Hooks with React Testing Library (renderHook)

@testing-library/react provides the specialized renderHook and act utilities to test custom hooks in a simulated component environment without writing manual boilerplate:

JavaScript
// src/hooks/__tests__/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import { useState, useCallback } from 'react';
 
// Sample custom hook to test
function useCounter(initialCount = 0) {
const [count, setCount] = useState(initialCount);
const increment = useCallback(() => setCount(c => c + 1), []);
const reset = useCallback(() => setCount(initialCount), [initialCount]);
return { count, increment, reset };
}
 
describe('useCounter Custom Hook', () => {
test('should initialize with provided initial count', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
 
test('should increment count correctly', () => {
const { result } = renderHook(() => useCounter(0));
 
// Wrap state-modifying actions inside act()
act(() => {
result.current.increment();
});
 
expect(result.current.count).toBe(1);
});
 
test('should reset count to initial value', () => {
const { result } = renderHook(() => useCounter(5));
 
act(() => {
result.current.increment();
result.current.reset();
});
 
expect(result.current.count).toBe(5);
});
});

4. Understanding act()

The act() wrapper ensures that all state updates, effects, and resulting Virtual DOM recalculations are fully flushed and processed synchronously before your test makes assertions.


Interactive Quiz

Practice Quiz

Question 1
Why can't a custom hook be tested by invoking it directly as a regular JavaScript function in a test file (e.g. const data = useFetch())?
Question 2
What utility provided by @testing-library/react allows testing custom hooks cleanly?
Question 3
What is the purpose of wrapping state-modifying hook actions inside act(() => { ... }) during unit testing?
Question 4
In const { result } = renderHook(() => useMyHook()), how do you access the hook's current return values?
Question 5
What is "Hook Composition"?
Next Lesson

React Error Boundaries and Lifecycle Catching

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext