Mastering React Hooks: Beyond useState and useEffect
React hooks revolutionized how we write React components. While most developers know useState and useEffect, there's a rich ecosystem of hooks that can dramatically improve your code.
Custom Hooks: The Real Power
Custom hooks are the key to reusable logic in React. Here are some patterns I've found invaluable:
1. useLocalStorage Hook
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue] as const;
}
2. useDebounce Hook
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
Advanced Built-in Hooks
useReducer for Complex State
When state logic becomes complex, useReducer shines:
interface State {
loading: boolean;
error: string | null;
data: any;
}
type Action =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: any }
| { type: 'FETCH_ERROR'; payload: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'FETCH_START':
return { loading: true, error: null, data: null };
case 'FETCH_SUCCESS':
return { loading: false, error: null, data: action.payload };
case 'FETCH_ERROR':
return { loading: false, error: action.payload, data: null };
default:
return state;
}
}
useCallback and useMemo
Performance optimization hooks that are often misunderstood:
// Memoize expensive calculations
const expensiveValue = useMemo(() => {
return data.reduce((sum, item) => sum + item.value, 0);
}, [data]);
// Memoize functions to prevent unnecessary re-renders
const handleClick = useCallback((id: string) => {
onItemClick(id);
}, [onItemClick]);
Context API Patterns
1. Split Contexts
Don't put everything in one context:
// Separate contexts for different concerns
const AuthContext = createContext<AuthState | null>(null);
const ThemeContext = createContext<ThemeState | null>(null);
const NotificationContext = createContext<NotificationState | null>(null);
2. Context Providers Composition
function AppProviders({ children }: { children: React.ReactNode }) {
return (
<AuthProvider>
<ThemeProvider>
<NotificationProvider>
{children}
</NotificationProvider>
</ThemeProvider>
</AuthProvider>
);
}
Testing Hooks
Testing custom hooks is crucial:
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Best Practices
- Keep hooks focused - Single responsibility principle
- Use TypeScript - Better type safety and IDE support
- Test your hooks - They're just functions
- Document usage - Custom hooks need clear documentation
- Consider performance - Not everything needs to be memoized
Common Pitfalls
- Overusing useMemo/useCallback - Profile before optimizing
- Dependencies arrays - Missing dependencies cause bugs
- Infinite loops - Effects that trigger themselves
- Stale closures - Not updating dependencies properly
Conclusion
React hooks are more than just a way to use state in functional components. They're a powerful pattern for composing logic, sharing code, and building maintainable applications.
Mastering hooks takes time, but the investment pays off in cleaner, more reusable code. Start with simple custom hooks and gradually build more complex ones as you understand the patterns.