React Hooks Deep Dive
React Hooks revolutionized how we write React components. Let's explore the most important hooks.
useState Hook
The most fundamental hook for managing component state:
jsx
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}useEffect Hook
Handles side effects in functional components:
jsx
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchUser() {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]); // Dependency array
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return <div>Welcome, {user.name}!</div>;
}Custom Hooks
Create reusable logic with custom hooks:
jsx
// Custom hook for API calls
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}
fetchData();
}, [url]);
return { data, loading, error };
}
// Using the custom hook
function UserList() {
const { data: users, loading, error } = useApi('/api/users');
if (loading) return <div>Loading users...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Advanced Hooks
useContext
Share data across components without prop drilling:
jsx
const ThemeContext = createContext();
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
function ThemedButton() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<button
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
>
Toggle Theme
</button>
);
}useReducer
For complex state management:
jsx
function todoReducer(state, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, { id: Date.now(), text: action.text, completed: false }];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
);
case 'DELETE_TODO':
return state.filter(todo => todo.id !== action.id);
default:
return state;
}
}
function TodoApp() {
const [todos, dispatch] = useReducer(todoReducer, []);
return (
<div>
<button onClick={() => dispatch({ type: 'ADD_TODO', text: 'New Todo' })}>
Add Todo
</button>
{todos.map(todo => (
<div key={todo.id}>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => dispatch({ type: 'TOGGLE_TODO', id: todo.id })}>
Toggle
</button>
</div>
))}
</div>
);
}Best Practices
- Only call hooks at the top level - Never inside loops, conditions, or nested functions
- Use dependency arrays correctly - Include all dependencies in useEffect
- Create custom hooks for reusable logic - Don't repeat yourself
- Use useCallback and useMemo for optimization - But only when necessary
Common Pitfalls
- Forgetting dependencies in useEffect
- Creating objects/functions in render causing unnecessary re-renders
- Not cleaning up subscriptions in useEffect
- Using hooks conditionally
Practice Projects
- Build a todo app with useReducer
- Create a custom hook for form validation
- Implement a theme switcher with useContext
- Build a data fetching component with error handling