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.
React Hooks were introduced in React 16.8 (2019) and have since become the standard way to write React components.
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
Essential Hook - Persist state across browser sessions:
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
Performance Hook - Delay expensive operations:
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 Hook Patterns
useAsync Hook
Handle async operations with loading states:
function useAsync<T, E = string>(
asyncFunction: () => Promise<T>,
immediate = true
) {
const [status, setStatus] = useState<'idle' | 'pending' | 'success' | 'error'>('idle');
const [value, setValue] = useState<T | null>(null);
const [error, setError] = useState<E | null>(null);
const execute = useCallback(async () => {
setStatus('pending');
setValue(null);
setError(null);
try {
const response = await asyncFunction();
setValue(response);
setStatus('success');
} catch (error) {
setError(error as E);
setStatus('error');
}
}, [asyncFunction]);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { execute, status, value, error };
}
usePrevious Hook
Track previous values:
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Hook Composition
The real power comes from combining hooks:
function useUserData(userId: string) {
const { value: user, status, error } = useAsync(() =>
fetchUser(userId)
);
const [settings, setSettings] = useLocalStorage('userSettings', {});
const debouncedSettings = useDebounce(settings, 500);
return {
user,
isLoading: status === 'pending',
error,
settings: debouncedSettings,
updateSettings: setSettings
};
}
Pro tip: Always start hooks with "use" and follow the rules of hooks - only call them at the top level!
Performance Optimizations
useMemo and useCallback
Performance - Prevent unnecessary re-renders:
function ExpensiveComponent({ data, onUpdate }) {
const expensiveValue = useMemo(() => {
return data.reduce((sum, item) => sum + item.value, 0);
}, [data]);
const handleClick = useCallback(() => {
onUpdate(expensiveValue);
}, [expensiveValue, onUpdate]);
return <div onClick={handleClick}>Total: {expensiveValue}</div>;
}
Best Practices
- Rule 1 - Always start custom hooks with "use"
- Rule 2 - Keep hooks small and focused
- Rule 3 - Use TypeScript for better type safety
- Rule 4 - Document your custom hooks
- Rule 5 - Test hooks independently
Real-World Examples
useApi Hook
function useApi<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
Conclusion
Mastering React hooks goes beyond knowing the basics. Custom hooks allow you to:
- ✓ Extract reusable logic
- ✓ Improve code organization
- ✓ Enhance performance
- ✓ Create better abstractions
Start building your own hook library and watch your React applications become more maintainable and powerful!
What custom hooks have you built? Share your favorites in the comments!