React Hooks revolutionized how we write components by enabling state management and side effects in functional components. Let's explore the most important hooks and learn how to use them effectively.
Understanding useState
The useState hook is your gateway to state management in functional components. It returns a stateful value and a function to update it.
1import { useState } from 'react'; 2 3function Counter() { 4 const [count, setCount] = useState(0); 5 6 return ( 7 <div> 8 <p>Count: {count}</p> 9 <button onClick={() => setCount(count + 1)}>Increment</button> 10 </div> 11 ); 12}
State Updates with Previous Value
When updating state based on its previous value, always use the functional update form:
1// 🚫 Don't do this 2setCount(count + 1); 3 4// ✅ Do this instead 5setCount(prevCount => prevCount + 1);
State with Objects
When working with object state, remember to spread the previous state:
1function UserProfile() { 2 const [user, setUser] = useState({ 3 name: 'John', 4 email: 'john@example.com' 5 }); 6 7 const updateEmail = (newEmail) => { 8 setUser(prevUser => ({ 9 ...prevUser, 10 email: newEmail 11 })); 12 }; 13}
Understanding useEffect
useEffect handles side effects in your components, like data fetching, subscriptions, or DOM manipulations.
Basic Usage
1function UserData({ userId }) { 2 const [user, setUser] = useState(null); 3 4 useEffect(() => { 5 async function fetchUser() { 6 const response = await fetch(`/api/users/${userId}`); 7 const data = await response.json(); 8 setUser(data); 9 } 10 11 fetchUser(); 12 }, [userId]); // Only re-run if userId changes 13}
Cleanup Functions
Always clean up side effects to prevent memory leaks:
1function ChatRoom({ roomId }) { 2 useEffect(() => { 3 const connection = createConnection(roomId); 4 connection.connect(); 5 6 // Cleanup function 7 return () => { 8 connection.disconnect(); 9 }; 10 }, [roomId]); 11}
Creating Custom Hooks
Custom hooks allow you to extract component logic into reusable functions. Here are some practical examples:
useLocalStorage Hook
1function useLocalStorage(key, initialValue) { 2 // State to store our value 3 // Pass initial state function to useState so logic is only executed once 4 const [storedValue, setStoredValue] = useState(() => { 5 try { 6 const item = window.localStorage.getItem(key); 7 return item ? JSON.parse(item) : initialValue; 8 } catch (error) { 9 console.error(error); 10 return initialValue; 11 } 12 }); 13 14 // Return a wrapped version of useState's setter function 15 const setValue = value => { 16 try { 17 // Allow value to be a function so we have same API as useState 18 const valueToStore = value instanceof Function ? value(storedValue) : value; 19 setStoredValue(valueToStore); 20 window.localStorage.setItem(key, JSON.stringify(valueToStore)); 21 } catch (error) { 22 console.error(error); 23 } 24 }; 25 26 return [storedValue, setValue]; 27}
useFetch Hook
1function useFetch(url) { 2 const [data, setData] = useState(null); 3 const [loading, setLoading] = useState(true); 4 const [error, setError] = useState(null); 5 6 useEffect(() => { 7 const fetchData = async () => { 8 try { 9 setLoading(true); 10 const response = await fetch(url); 11 const json = await response.json(); 12 setData(json); 13 setError(null); 14 } catch (err) { 15 setError(err.message); 16 } finally { 17 setLoading(false); 18 } 19 }; 20 21 fetchData(); 22 }, [url]); 23 24 return { data, loading, error }; 25}
Common Patterns
Conditional Fetching
1function SearchResults({ query }) { 2 const [results, setResults] = useState([]); 3 4 useEffect(() => { 5 // Don't fetch if query is empty 6 if (!query.trim()) { 7 setResults([]); 8 return; 9 } 10 11 const fetchResults = async () => { 12 const data = await searchApi(query); 13 setResults(data); 14 }; 15 16 fetchResults(); 17 }, [query]); 18}
Debounced Updates
1function useDebounce(value, delay) { 2 const [debouncedValue, setDebouncedValue] = useState(value); 3 4 useEffect(() => { 5 const handler = setTimeout(() => { 6 setDebouncedValue(value); 7 }, delay); 8 9 return () => { 10 clearTimeout(handler); 11 }; 12 }, [value, delay]); 13 14 return debouncedValue; 15}
Best Practices
1. Dependencies Array Management
Always specify all dependencies used inside useEffect:
1// 🚫 Don't do this 2useEffect(() => { 3 fetchData(userId, filters); 4}, []); // Missing dependencies 5 6// ✅ Do this instead 7useEffect(() => { 8 fetchData(userId, filters); 9}, [userId, filters]); // All dependencies listed
2. Avoid Infinite Loops
Be careful with object and array dependencies:
1// 🚫 Don't do this 2useEffect(() => { 3 fetchData({ id: userId }); 4}, [{ id: userId }]); // New object created each render 5 6// ✅ Do this instead 7useEffect(() => { 8 fetchData({ id: userId }); 9}, [userId]); // Use primitive values in dependencies
3. Use Multiple Effects for Different Concerns
1function UserProfile({ userId }) { 2 // Separate effects for different concerns 3 useEffect(() => { 4 // Handle user data 5 }, [userId]); 6 7 useEffect(() => { 8 // Handle user preferences 9 }, [userId]); 10 11 useEffect(() => { 12 // Handle notifications 13 return () => { 14 // Cleanup notifications 15 }; 16 }, []); 17}
4. Early Returns in Effects
1useEffect(() => { 2 if (!userId) return; // Early return if no userId 3 4 const fetchUser = async () => { 5 // Fetch user data 6 }; 7 8 fetchUser(); 9}, [userId]);
Conclusion
React Hooks provide a powerful way to manage state and side effects in functional components. By following these patterns and best practices, you can write more maintainable and efficient React applications. Remember to:
- Use the functional update form when updating state based on previous values
- Always clean up side effects
- Create custom hooks to reuse logic
- Manage dependencies carefully
- Keep effects focused on single concerns
- Handle edge cases and loading states
The more you work with hooks, the more natural they become. Happy coding! ☕☕