Skip to main content

Chapter 5: Advanced Hooks - Mastering React's Powerful Hook System

Welcome to the comprehensive guide to advanced React hooks! In this chapter, we'll explore the most powerful and sophisticated hooks that React provides, along with advanced patterns for building complex, performant applications.

Learning Objectives

By the end of this chapter, you will understand:

  • What advanced hooks are and why they're essential for modern React development
  • How to use useMemo and useCallback for performance optimization
  • Why useRef is powerful and how to use it for DOM manipulation and persistent values
  • What custom hooks are and how to create reusable logic
  • How to compose hooks for complex functionality
  • Why performance matters and how to optimize with hooks
  • What common pitfalls exist and how to avoid them

What are Advanced Hooks? The Foundation of Modern React Development

What are Advanced Hooks?

Advanced hooks are React's most powerful tools for managing complex state, optimizing performance, and creating reusable logic. They go beyond the basic useState and useEffect to provide sophisticated capabilities for building enterprise-grade applications.

Advanced hooks are what separate beginner React developers from experts. They enable you to build performant, maintainable, and scalable applications with clean, reusable code.

What Makes Hooks "Advanced"?

Advanced hooks provide capabilities that basic hooks cannot:

  1. Performance Optimization: useMemo and useCallback prevent unnecessary re-renders
  2. DOM Manipulation: useRef provides direct access to DOM elements
  3. Complex State Logic: useReducer handles complex state transitions
  4. Global State Management: useContext enables state sharing across components
  5. Custom Logic Reuse: Custom hooks encapsulate and share complex logic
  6. Imperative APIs: useImperativeHandle exposes imperative methods to parent components

What Problems Do Advanced Hooks Solve?

Advanced hooks address common challenges in React development:

  • Performance Issues: Unnecessary re-renders and expensive calculations
  • Code Duplication: Repeated logic across multiple components
  • Complex State Management: Managing interrelated state updates
  • DOM Access: Direct manipulation of DOM elements
  • Side Effect Management: Complex asynchronous operations and cleanup

How to Optimize Performance? useMemo and useCallback Deep Dive

What is useMemo?

useMemo is a React hook that memoizes the result of a computation and only recalculates it when its dependencies change. It's essential for optimizing expensive calculations.

useMemo is what prevents your app from recalculating expensive operations on every render.

How to Use useMemo for Expensive Calculations?

import React, { useState, useMemo } from 'react';

function ExpensiveCalculation({ items, filter }) {
// This expensive calculation will only run when items or filter changes
const filteredItems = useMemo(() => {
console.log('Filtering items...'); // This will only log when dependencies change
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);

// Another expensive calculation
const statistics = useMemo(() => {
console.log('Calculating statistics...');
return {
total: filteredItems.length,
average: filteredItems.reduce((sum, item) => sum + item.value, 0) / filteredItems.length,
max: Math.max(...filteredItems.map(item => item.value)),
min: Math.min(...filteredItems.map(item => item.value))
};
}, [filteredItems]);

return (
<div>
<h3>Filtered Items ({statistics.total})</h3>
<p>Average: {statistics.average.toFixed(2)}</p>
<p>Max: {statistics.max}</p>
<p>Min: {statistics.min}</p>
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}: {item.value}</li>
))}
</ul>
</div>
);
}

function App() {
const [items, setItems] = useState([
{ id: 1, name: 'Apple', value: 10 },
{ id: 2, name: 'Banana', value: 20 },
{ id: 3, name: 'Cherry', value: 15 },
{ id: 4, name: 'Date', value: 25 }
]);
const [filter, setFilter] = useState('');
const [count, setCount] = useState(0);

return (
<div>
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter items..."
/>
<button onClick={() => setCount(count + 1)}>
Re-render count: {count}
</button>
<ExpensiveCalculation items={items} filter={filter} />
</div>
);
}

What is useCallback?

useCallback is a React hook that memoizes a function and only recreates it when its dependencies change. It's essential for preventing unnecessary re-renders of child components.

useCallback is what prevents your functions from being recreated on every render, which can cause child components to re-render unnecessarily.

How to Use useCallback for Function Memoization?

import React, { useState, useCallback, memo } from 'react';

// Memoized child component
const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete, onEdit }) {
console.log(`Rendering TodoItem ${todo.id}`); // This will only log when props change

return (
<div style={{
display: 'flex',
alignItems: 'center',
padding: '8px',
border: '1px solid #ccc',
margin: '4px 0'
}}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span style={{
flex: 1,
textDecoration: todo.completed ? 'line-through' : 'none',
margin: '0 8px'
}}>
{todo.text}
</span>
<button onClick={() => onEdit(todo.id)}>Edit</button>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
);
});

function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Build an app', completed: true },
{ id: 3, text: 'Deploy to production', completed: false }
]);
const [newTodo, setNewTodo] = useState('');
const [filter, setFilter] = useState('all');

// Memoized functions - these will only be recreated when todos changes
const handleToggle = useCallback((id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);

const handleDelete = useCallback((id) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
}, []);

const handleEdit = useCallback((id) => {
const newText = prompt('Enter new text:');
if (newText) {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
)
);
}
}, []);

const handleAdd = useCallback(() => {
if (newTodo.trim()) {
setTodos(prevTodos => [
...prevTodos,
{
id: Date.now(),
text: newTodo.trim(),
completed: false
}
]);
setNewTodo('');
}
}, [newTodo]);

// Filtered todos - memoized to prevent unnecessary filtering
const filteredTodos = useMemo(() => {
switch (filter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
}, [todos, filter]);

return (
<div>
<h2>Todo List</h2>

<div>
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add new todo..."
/>
<button onClick={handleAdd}>Add</button>
</div>

<div>
<button
onClick={() => setFilter('all')}
style={{ backgroundColor: filter === 'all' ? '#007bff' : '#f8f9fa' }}
>
All
</button>
<button
onClick={() => setFilter('active')}
style={{ backgroundColor: filter === 'active' ? '#007bff' : '#f8f9fa' }}
>
Active
</button>
<button
onClick={() => setFilter('completed')}
style={{ backgroundColor: filter === 'completed' ? '#007bff' : '#f8f9fa' }}
>
Completed
</button>
</div>

<div>
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
onEdit={handleEdit}
/>
))}
</div>
</div>
);
}

Why Use useMemo and useCallback? The Performance Benefits Explained

Why is useMemo Important for Performance?

useMemo prevents expensive calculations from running on every render:

Without useMemo (inefficient):

function ExpensiveComponent({ data }) {
// This expensive calculation runs on EVERY render
const expensiveValue = data.reduce((sum, item) => {
// Simulate expensive calculation
for (let i = 0; i < 1000000; i++) {
sum += Math.random();
}
return sum + item.value;
}, 0);

return <div>Expensive value: {expensiveValue}</div>;
}

With useMemo (optimized):

function ExpensiveComponent({ data }) {
// This expensive calculation only runs when data changes
const expensiveValue = useMemo(() => {
return data.reduce((sum, item) => {
// Simulate expensive calculation
for (let i = 0; i < 1000000; i++) {
sum += Math.random();
}
return sum + item.value;
}, 0);
}, [data]);

return <div>Expensive value: {expensiveValue}</div>;
}

Why is useCallback Important for Child Components?

useCallback prevents unnecessary re-renders of child components:

Without useCallback (causes unnecessary re-renders):

function Parent() {
const [count, setCount] = useState(0);

// This function is recreated on every render
const handleClick = () => {
console.log('Button clicked');
};

return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<Child onClick={handleClick} /> {/* Re-renders every time */}
</div>
);
}

With useCallback (prevents unnecessary re-renders):

function Parent() {
const [count, setCount] = useState(0);

// This function is only recreated when dependencies change
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []);

return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<Child onClick={handleClick} /> {/* Only re-renders when onClick changes */}
</div>
);
}

When Should You Use useMemo and useCallback?

Use useMemo when:

  • You have expensive calculations that depend on specific values
  • You're creating objects or arrays that are passed as props
  • You want to prevent unnecessary recalculations

Use useCallback when:

  • You're passing functions as props to child components
  • You're using functions in dependency arrays of other hooks
  • You want to prevent unnecessary re-renders of child components

Don't use them when:

  • The calculation is simple and fast
  • The dependencies change on every render anyway
  • You're over-optimizing and making code harder to read

How to Access DOM Elements? useRef Deep Dive

What is useRef?

useRef is a React hook that returns a mutable ref object whose .current property is initialized to the passed argument. It's used for accessing DOM elements and storing mutable values that don't trigger re-renders.

useRef is what gives you direct access to DOM elements and allows you to store values that persist across renders without causing re-renders.

// Todo reducer with multiple actions
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, {
id: Date.now(),
text: action.payload,
completed: false,
createdAt: new Date().toISOString()
}]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
case 'EDIT_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, text: action.payload.text }
: todo
)
};
case 'SET_FILTER':
return { ...state, filter: action.payload };
case 'CLEAR_COMPLETED':
return {
...state,
todos: state.todos.filter(todo => !todo.completed)
};
default:
return state;
}
}

function TodoApp() {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
filter: 'all'
});

const addTodo = (text) => {
dispatch({ type: 'ADD_TODO', payload: text });
};

const toggleTodo = (id) => {
dispatch({ type: 'TOGGLE_TODO', payload: id });
};

const deleteTodo = (id) => {
dispatch({ type: 'DELETE_TODO', payload: id });
};

const editTodo = (id, text) => {
dispatch({ type: 'EDIT_TODO', payload: { id, text } });
};

const setFilter = (filter) => {
dispatch({ type: 'SET_FILTER', payload: filter });
};

const clearCompleted = () => {
dispatch({ type: 'CLEAR_COMPLETED' });
};

return (
<div>
<TodoInput onAdd={addTodo} />
<TodoFilter filter={state.filter} onFilterChange={setFilter} />
<TodoList
todos={state.todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
onEdit={editTodo}
/>
<button onClick={clearCompleted}>Clear Completed</button>
</div>
);
}

useContext Hook

useContext allows you to consume context values in functional components.

Creating and Using Context

import React, { createContext, useContext, useState } from 'react';

// Create context
const ThemeContext = createContext();

// Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');

const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};

const value = {
theme,
toggleTheme,
colors: {
light: { bg: '#fff', text: '#000' },
dark: { bg: '#333', text: '#fff' }
}
};

return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}

// Custom hook
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

// Components using the context
function App() {
return (
<ThemeProvider>
<Header />
<Main />
</ThemeProvider>
);
}

function Header() {
const { theme, toggleTheme } = useTheme();

return (
<header style={{ backgroundColor: theme === 'light' ? '#fff' : '#333' }}>
<h1>My App</h1>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} theme
</button>
</header>
);
}

function Main() {
const { theme, colors } = useTheme();
const currentColors = colors[theme];

return (
<main style={{
backgroundColor: currentColors.bg,
color: currentColors.text
}}>
<p>This is the main content with {theme} theme.</p>
</main>
);
}

Multiple Contexts

// User context
const UserContext = createContext();

function UserProvider({ children }) {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(false);

const login = async (credentials) => {
setIsLoading(true);
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
});
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('Login failed:', error);
} finally {
setIsLoading(false);
}
};

const logout = () => {
setUser(null);
};

return (
<UserContext.Provider value={{ user, login, logout, isLoading }}>
{children}
</UserContext.Provider>
);
}

// Notification context
const NotificationContext = createContext();

function NotificationProvider({ children }) {
const [notifications, setNotifications] = useState([]);

const addNotification = (notification) => {
const id = Date.now();
setNotifications(prev => [...prev, { ...notification, id }]);

// Auto-remove after 5 seconds
setTimeout(() => {
removeNotification(id);
}, 5000);
};

const removeNotification = (id) => {
setNotifications(prev => prev.filter(n => n.id !== id));
};

return (
<NotificationContext.Provider value={{
notifications,
addNotification,
removeNotification
}}>
{children}
</NotificationContext.Provider>
);
}

// App with multiple contexts
function App() {
return (
<UserProvider>
<NotificationProvider>
<ThemeProvider>
<Header />
<Main />
<NotificationList />
</ThemeProvider>
</NotificationProvider>
</UserProvider>
);
}

useMemo Hook

useMemo memoizes expensive calculations to prevent unnecessary recalculations.

Basic useMemo

import React, { useState, useMemo } from 'react';

function ExpensiveComponent({ items, filter }) {
// This calculation will only run when items or filter changes
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);

// This calculation will only run when filteredItems changes
const totalPrice = useMemo(() => {
console.log('Calculating total...');
return filteredItems.reduce((sum, item) => sum + item.price, 0);
}, [filteredItems]);

return (
<div>
<h3>Filtered Items ({filteredItems.length})</h3>
<ul>
{filteredItems.map(item => (
<li key={item.id}>
{item.name} - ${item.price}
</li>
))}
</ul>
<p>Total: ${totalPrice}</p>
</div>
);
}

function App() {
const [items] = useState([
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Mouse', price: 25 },
{ id: 3, name: 'Keyboard', price: 75 }
]);
const [filter, setFilter] = useState('');
const [count, setCount] = useState(0);

return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter items..."
/>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<ExpensiveComponent items={items} filter={filter} />
</div>
);
}

useMemo with Objects

function UserProfile({ user, preferences }) {
// Memoize the user display object
const userDisplay = useMemo(() => ({
name: user.name,
email: user.email,
avatar: user.avatar || '/default-avatar.png',
theme: preferences.theme,
notifications: preferences.notifications
}), [user.name, user.email, user.avatar, preferences.theme, preferences.notifications]);

return (
<div className={`profile ${userDisplay.theme}`}>
<img src={userDisplay.avatar} alt={userDisplay.name} />
<h2>{userDisplay.name}</h2>
<p>{userDisplay.email}</p>
<p>Notifications: {userDisplay.notifications ? 'On' : 'Off'}</p>
</div>
);
}

useCallback Hook

useCallback memoizes functions to prevent unnecessary re-renders of child components.

Basic useCallback

import React, { useState, useCallback, memo } from 'react';

// Memoized child component
const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete }) {
console.log(`Rendering todo: ${todo.text}`);

return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</li>
);
});

function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Build an app', completed: true }
]);
const [filter, setFilter] = useState('all');

// Memoized callback functions
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}, []);

const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);

const filteredTodos = todos.filter(todo => {
switch (filter) {
case 'active': return !todo.completed;
case 'completed': return todo.completed;
default: return true;
}
});

return (
<div>
<div>
<button onClick={() => setFilter('all')}>All</button>
<button onClick={() => setFilter('active')}>Active</button>
<button onClick={() => setFilter('completed')}>Completed</button>
</div>
<ul>
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
</div>
);
}

useCallback with Dependencies

function SearchableList({ items, onItemSelect }) {
const [searchTerm, setSearchTerm] = useState('');
const [selectedItems, setSelectedItems] = useState([]);

// Memoized search function
const handleSearch = useCallback((term) => {
setSearchTerm(term);
}, []);

// Memoized selection handler
const handleItemSelect = useCallback((item) => {
setSelectedItems(prev => {
const isSelected = prev.some(selected => selected.id === item.id);
if (isSelected) {
return prev.filter(selected => selected.id !== item.id);
} else {
return [...prev, item];
}
});
onItemSelect(item);
}, [onItemSelect]);

const filteredItems = items.filter(item =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);

return (
<div>
<input
value={searchTerm}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search items..."
/>
<ul>
{filteredItems.map(item => (
<li
key={item.id}
onClick={() => handleItemSelect(item)}
className={selectedItems.some(selected => selected.id === item.id) ? 'selected' : ''}
>
{item.name}
</li>
))}
</ul>
</div>
);
}

useRef Hook

useRef provides a way to access DOM elements and persist values across renders.

DOM Manipulation

import React, { useRef, useEffect } from 'react';

function FocusInput() {
const inputRef = useRef();

useEffect(() => {
// Focus the input when component mounts
inputRef.current.focus();
}, []);

const handleClick = () => {
inputRef.current.focus();
};

return (
<div>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>Focus Input</button>
</div>
);
}

function ScrollToTop() {
const topRef = useRef();

const scrollToTop = () => {
topRef.current.scrollIntoView({ behavior: 'smooth' });
};

return (
<div>
<div ref={topRef}>Top of page</div>
<div style={{ height: '2000px' }}>
<p>Long content...</p>
<button onClick={scrollToTop}>Scroll to Top</button>
</div>
</div>
);
}

Persistent Values

function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef();

const startTimer = () => {
if (intervalRef.current) return; // Prevent multiple intervals

intervalRef.current = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
};

const stopTimer = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};

const resetTimer = () => {
stopTimer();
setCount(0);
};

useEffect(() => {
return () => {
// Cleanup on unmount
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);

return (
<div>
<h2>Timer: {count}s</h2>
<button onClick={startTimer}>Start</button>
<button onClick={stopTimer}>Stop</button>
<button onClick={resetTimer}>Reset</button>
</div>
);
}

Previous Value Tracking

function usePrevious(value) {
const ref = useRef();

useEffect(() => {
ref.current = value;
});

return ref.current;
}

function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);

return (
<div>
<h2>Current: {count}</h2>
<h3>Previous: {prevCount}</h3>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

useImperativeHandle Hook

useImperativeHandle customizes the instance value that is exposed to parent components when using refs.

import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';

// Child component with imperative handle
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
const [value, setValue] = useState('');

useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
blur: () => {
inputRef.current.blur();
},
clear: () => {
setValue('');
inputRef.current.focus();
},
getValue: () => value,
setValue: (newValue) => setValue(newValue)
}));

return (
<input
ref={inputRef}
value={value}
onChange={(e) => setValue(e.target.value)}
{...props}
/>
);
});

// Parent component
function Form() {
const inputRef = useRef();

const handleFocus = () => {
inputRef.current.focus();
};

const handleClear = () => {
inputRef.current.clear();
};

const handleGetValue = () => {
const value = inputRef.current.getValue();
alert(`Input value: ${value}`);
};

return (
<div>
<FancyInput ref={inputRef} placeholder="Enter text..." />
<button onClick={handleFocus}>Focus Input</button>
<button onClick={handleClear}>Clear Input</button>
<button onClick={handleGetValue}>Get Value</button>
</div>
);
}

Custom Hooks

Custom hooks allow you to extract component logic into reusable functions.

Data Fetching Hook

import { useState, useEffect } from 'react';

function useApi(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
let cancelled = false;

const fetchData = async () => {
try {
setLoading(true);
setError(null);

const response = await fetch(url, options);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const result = await response.json();

if (!cancelled) {
setData(result);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};

fetchData();

return () => {
cancelled = true;
};
}, [url, JSON.stringify(options)]);

const refetch = () => {
setLoading(true);
setError(null);
// Trigger useEffect by changing a dependency
};

return { data, loading, error, refetch };
}

// Usage
function UserProfile({ userId }) {
const { data: user, loading, error } = useApi(`/api/users/${userId}`);

if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>User not found</div>;

return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
</div>
);
}

Local Storage Hook

function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});

const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};

return [storedValue, setValue];
}

// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'en');

return (
<div>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
</select>
</div>
);
}

Debounced Value Hook

import { useState, useEffect } from 'react';

function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);

return () => {
clearTimeout(handler);
};
}, [value, delay]);

return debouncedValue;
}

// Usage
function SearchInput() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);

useEffect(() => {
if (debouncedSearchTerm) {
// Perform search API call
console.log('Searching for:', debouncedSearchTerm);
}
}, [debouncedSearchTerm]);

return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}

Hook Composition Patterns

Combining Multiple Hooks

function useUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

if (userId) {
fetchUser();
}
}, [userId]);

return { user, loading, error };
}

function useUserPosts(userId) {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
const fetchPosts = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}/posts`);
const postsData = await response.json();
setPosts(postsData);
} catch (err) {
console.error('Error fetching posts:', err);
} finally {
setLoading(false);
}
};

if (userId) {
fetchPosts();
}
}, [userId]);

return { posts, loading };
}

// Component using multiple custom hooks
function UserProfile({ userId }) {
const { user, loading: userLoading, error } = useUser(userId);
const { posts, loading: postsLoading } = useUserPosts(userId);

if (userLoading) return <div>Loading user...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>User not found</div>;

return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>

<h3>Posts</h3>
{postsLoading ? (
<div>Loading posts...</div>
) : (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)}
</div>
);
}

Practice Project: Advanced Todo App

Let's build an advanced todo application using all the hooks we've learned:

import React, { useState, useReducer, useCallback, useMemo, useRef, useEffect } from 'react';

// Reducer for todo state
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, {
id: Date.now(),
text: action.payload.text,
completed: false,
priority: action.payload.priority || 'medium',
createdAt: new Date().toISOString()
}]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
case 'EDIT_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload.id
? { ...todo, text: action.payload.text }
: todo
)
};
case 'SET_FILTER':
return { ...state, filter: action.payload };
case 'SET_SORT':
return { ...state, sortBy: action.payload };
default:
return state;
}
}

// Custom hook for todos
function useTodos() {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
filter: 'all',
sortBy: 'created'
});

const addTodo = useCallback((text, priority) => {
dispatch({ type: 'ADD_TODO', payload: { text, priority } });
}, []);

const toggleTodo = useCallback((id) => {
dispatch({ type: 'TOGGLE_TODO', payload: id });
}, []);

const deleteTodo = useCallback((id) => {
dispatch({ type: 'DELETE_TODO', payload: id });
}, []);

const editTodo = useCallback((id, text) => {
dispatch({ type: 'EDIT_TODO', payload: { id, text } });
}, []);

const setFilter = useCallback((filter) => {
dispatch({ type: 'SET_FILTER', payload: filter });
}, []);

const setSort = useCallback((sortBy) => {
dispatch({ type: 'SET_SORT', payload: sortBy });
}, []);

return {
...state,
addTodo,
toggleTodo,
deleteTodo,
editTodo,
setFilter,
setSort
};
}

// Memoized todo item component
const TodoItem = React.memo(function TodoItem({ todo, onToggle, onDelete, onEdit }) {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(todo.text);
const inputRef = useRef();

useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
}, [isEditing]);

const handleSave = useCallback(() => {
if (editText.trim()) {
onEdit(todo.id, editText.trim());
setIsEditing(false);
}
}, [todo.id, editText, onEdit]);

const handleKeyPress = useCallback((e) => {
if (e.key === 'Enter') {
handleSave();
} else if (e.key === 'Escape') {
setEditText(todo.text);
setIsEditing(false);
}
}, [handleSave, todo.text]);

return (
<li className={`todo-item ${todo.priority} ${todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>

{isEditing ? (
<input
ref={inputRef}
value={editText}
onChange={(e) => setEditText(e.target.value)}
onBlur={handleSave}
onKeyDown={handleKeyPress}
/>
) : (
<span onDoubleClick={() => setIsEditing(true)}>
{todo.text}
</span>
)}

<div className="todo-actions">
<button onClick={() => setIsEditing(true)}>Edit</button>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
</li>
);
});

// Main todo app
function AdvancedTodoApp() {
const {
todos,
filter,
sortBy,
addTodo,
toggleTodo,
deleteTodo,
editTodo,
setFilter,
setSort
} = useTodos();

const [newTodoText, setNewTodoText] = useState('');
const [newTodoPriority, setNewTodoPriority] = useState('medium');

// Memoized filtered and sorted todos
const filteredAndSortedTodos = useMemo(() => {
let filtered = todos;

// Filter
switch (filter) {
case 'active':
filtered = todos.filter(todo => !todo.completed);
break;
case 'completed':
filtered = todos.filter(todo => todo.completed);
break;
default:
filtered = todos;
}

// Sort
return filtered.sort((a, b) => {
switch (sortBy) {
case 'title':
return a.text.localeCompare(b.text);
case 'priority':
const priorityOrder = { high: 3, medium: 2, low: 1 };
return priorityOrder[b.priority] - priorityOrder[a.priority];
case 'created':
default:
return new Date(b.createdAt) - new Date(a.createdAt);
}
});
}, [todos, filter, sortBy]);

// Memoized statistics
const stats = useMemo(() => ({
total: todos.length,
completed: todos.filter(todo => todo.completed).length,
active: todos.filter(todo => !todo.completed).length,
byPriority: todos.reduce((acc, todo) => {
acc[todo.priority] = (acc[todo.priority] || 0) + 1;
return acc;
}, {})
}), [todos]);

const handleAddTodo = useCallback((e) => {
e.preventDefault();
if (newTodoText.trim()) {
addTodo(newTodoText.trim(), newTodoPriority);
setNewTodoText('');
}
}, [newTodoText, newTodoPriority, addTodo]);

return (
<div className="advanced-todo-app">
<h1>Advanced Todo App</h1>

{/* Add todo form */}
<form onSubmit={handleAddTodo} className="add-todo-form">
<input
type="text"
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
placeholder="Add a new todo..."
/>
<select
value={newTodoPriority}
onChange={(e) => setNewTodoPriority(e.target.value)}
>
<option value="low">Low Priority</option>
<option value="medium">Medium Priority</option>
<option value="high">High Priority</option>
</select>
<button type="submit">Add Todo</button>
</form>

{/* Filters and sorting */}
<div className="controls">
<div className="filters">
<label>Filter:</label>
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
</div>

<div className="sorting">
<label>Sort by:</label>
<select value={sortBy} onChange={(e) => setSort(e.target.value)}>
<option value="created">Created Date</option>
<option value="title">Title</option>
<option value="priority">Priority</option>
</select>
</div>
</div>

{/* Todo list */}
<ul className="todo-list">
{filteredAndSortedTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
onEdit={editTodo}
/>
))}
</ul>

{/* Statistics */}
<div className="stats">
<h3>Statistics</h3>
<div className="stats-grid">
<div>Total: {stats.total}</div>
<div>Active: {stats.active}</div>
<div>Completed: {stats.completed}</div>
{Object.entries(stats.byPriority).map(([priority, count]) => (
<div key={priority}>
{priority.charAt(0).toUpperCase() + priority.slice(1)}: {count}
</div>
))}
</div>
</div>
</div>
);
}

export default AdvancedTodoApp;

Summary

In this chapter, we explored advanced React hooks:

  • useReducer: Complex state management with actions and reducers
  • useContext: Global state sharing without prop drilling
  • useMemo: Memoizing expensive calculations
  • useCallback: Memoizing functions to prevent unnecessary re-renders
  • useRef: DOM manipulation and persistent values
  • useImperativeHandle: Customizing ref behavior
  • Custom Hooks: Reusable logic extraction
  • Hook Composition: Combining multiple hooks effectively

Next Steps

Additional Resources