React Hooks Deep Dive: Beyond the Basics
React Hooks revolutionized how we write React components, but there's so much more to explore beyond useState and useEffect. Let's dive deep into advanced patterns and techniques that will level up your React skills.
Custom Hooks: The Power of Abstraction
Custom hooks are JavaScript functions whose names start with "use" and that can call other hooks. They're perfect for extracting component logic into reusable functions.
The Anatomy of a Custom Hook
// useLocalStorage.js
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Get stored value from localStorage or use initialValue
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function
const setValue = (value) => {
try {
// Allow value to be a function
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue];
}
// Usage in component
function App() {
const [name, setName] = useLocalStorage('name', 'John');
return (
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
}
Advanced Custom Hook Patterns
1. useAsync Hook for Data Fetching
function useAsync(asyncFunction, dependencies = []) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isCancelled = false;
async function fetchData() {
try {
setLoading(true);
const result = await asyncFunction();
if (!isCancelled) {
setData(result);
setError(null);
}
} catch (err) {
if (!isCancelled) {
setError(err);
setData(null);
}
} finally {
if (!isCancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
isCancelled = true;
};
}, dependencies);
return { data, loading, error };
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error } = useAsync(
() => fetchUser(userId),
[userId]
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>No user found</div>;
return <div>{user.name}</div>;
}
2. useDebounce Hook for Performance
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Usage for search input
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
// Make API call here
searchAPI(debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}
Advanced Hook Patterns
1. useReducer for Complex State
function complexReducer(state, action) {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, loading: false, data: action.payload };
case 'FETCH_ERROR':
return { ...state, loading: false, error: action.payload };
case 'RESET':
return { loading: false, data: null, error: null };
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
}
function useComplexApi() {
const [state, dispatch] = useReducer(complexReducer, {
loading: false,
data: null,
error: null
});
const fetchData = async (url) => {
dispatch({ type: 'FETCH_START' });
try {
const response = await fetch(url);
const data = await response.json();
dispatch({ type: 'FETCH_SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'FETCH_ERROR', payload: error.message });
}
};
return { ...state, fetchData, reset: () => dispatch({ type: 'RESET' }) };
}
2. useContext for Global State
// ThemeContext.js
import React, { createContext, useContext, useReducer } from 'react';
const ThemeContext = createContext();
function themeReducer(state, action) {
switch (action.type) {
case 'TOGGLE_THEME':
return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
case 'SET_PRIMARY_COLOR':
return { ...state, primaryColor: action.payload };
default:
return state;
}
}
export function ThemeProvider({ children }) {
const [state, dispatch] = useReducer(themeReducer, {
theme: 'light',
primaryColor: '#blue'
});
return (
<ThemeContext.Provider value={{ ...state, dispatch }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// Usage
function ThemedComponent() {
const { theme, primaryColor, dispatch } = useTheme();
return (
<div style={{ background: theme === 'dark' ? '#333' : '#fff' }}>
<button
onClick={() => dispatch({ type: 'TOGGLE_THEME' })}
style={{ backgroundColor: primaryColor }}
>
Toggle Theme
</button>
</div>
);
}
Performance Optimization
1. useMemo for Expensive Calculations
function ExpensiveComponent({ data, filters }) {
const filteredAndProcessedData = useMemo(() => {
return data
.filter(item => {
// Complex filtering logic
return Object.keys(filters).every(key =>
item[key] === filters[key]
);
})
.map(item => {
// Complex data transformation
return {
...item,
computedValue: complexCalculation(item)
};
})
.sort((a, b) => b.computedValue - a.computedValue);
}, [data, filters]);
return <DataList data={filteredAndProcessedData} />;
}
2. useCallback for Function Optimization
function TodoList({ todos, onToggle }) {
const [filter, setFilter] = useState('');
const handleToggle = useCallback((id) => {
onToggle(id);
}, [onToggle]);
const filteredTodos = useMemo(() => {
return todos.filter(todo =>
todo.text.toLowerCase().includes(filter.toLowerCase())
);
}, [todos, filter]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter todos..."
/>
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
/>
))}
</div>
);
}
Best Practices
- Follow the Rules of Hooks: Only call hooks at the top level, only call hooks from React functions
- Extract Complex Logic: Create custom hooks for reusable logic
- Optimize Performance: Use useMemo and useCallback when necessary
- Handle Cleanup: Always cleanup side effects in useEffect
- Type Safety: Use TypeScript with hooks for better type safety
Conclusion
React Hooks provide a powerful and flexible way to build React applications. By mastering advanced patterns and custom hooks, you can create more maintainable, reusable, and performant components.
Remember, the key to great hook usage is abstraction and composition. Start simple, and gradually extract more complex logic into custom hooks as your components grow.