React Hooks: A Complete Guide for Modern Development
React Hooks revolutionized how we write React components by allowing us to use state and lifecycle features in functional components. This guide covers everything you need to know about React Hooks.
What Are React Hooks?
Hooks are functions that let you βhook intoβ React features from functional components. They were introduced in React 16.8 and have become the standard way to write React components.
Benefits of Hooks
- Simpler component logic
- Better code reusability
- Easier testing
- Reduced component nesting
- More intuitive state management
Essential React Hooks
useState Hook
The useState hook manages local state in functional components:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
useEffect Hook
The useEffect hook handles side effects in functional components:
import React, { 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('Error fetching 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>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
useContext Hook
The useContext hook provides a way to pass data through the component tree:
import React, { createContext, useContext, useState } from 'react';
// Create context
const ThemeContext = createContext();
// Provider component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Consumer component
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#333' : '#fff'
}}
>
Toggle Theme
</button>
);
}
Advanced Hooks
useReducer Hook
For complex state logic, useReducer is often preferable to useState:
import React, { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
case 'reset':
return initialState;
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>
+
</button>
<button onClick={() => dispatch({ type: 'decrement' })}>
-
</button>
<button onClick={() => dispatch({ type: 'reset' })}>
Reset
</button>
</div>
);
}
useMemo and useCallback
Optimization hooks for expensive calculations and function references:
import React, { useState, useMemo, useCallback } from 'react';
function ExpensiveComponent({ items, onItemClick }) {
const [filter, setFilter] = useState('');
// Memoize expensive calculation
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
// Memoize callback function
const handleItemClick = useCallback((item) => {
onItemClick(item);
}, [onItemClick]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter items..."
/>
{filteredItems.map(item => (
<div key={item.id} onClick={() => handleItemClick(item)}>
{item.name}
</div>
))}
</div>
);
}
Custom Hooks
Create reusable logic with custom hooks:
// 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);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage
function UserList() {
const { data: users, loading, error } = useApi('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Best Practices
Hook Rules
- Only call hooks at the top level
- Only call hooks from React functions
- Use ESLint plugin for hooks
Performance Tips
- Use
useCallbackfor event handlers passed to child components - Use
useMemofor expensive calculations - Optimize dependency arrays in
useEffect - Consider
React.memofor component memoization
Common Pitfalls
- Missing dependencies in
useEffect - Infinite re-renders due to object/array dependencies
- Overusing
useCallbackanduseMemo - Not cleaning up subscriptions in
useEffect
Conclusion
React Hooks provide a powerful and flexible way to manage state and side effects in functional components. They promote code reusability and make components easier to understand and test.
Start with useState and useEffect, then gradually incorporate other hooks as needed. Remember to follow the rules of hooks and consider performance implications when building complex applications.
Hooks have fundamentally changed how we write React applications, making them more functional and composable. Embrace this paradigm shift and enjoy writing cleaner, more maintainable React code!