5 Essential React Hooks Every Developer Should Master
React Hooks revolutionized how we write React components, making functional components as powerful as class components while keeping the code cleaner and more reusable. In this post, we'll dive deep into 5 essential hooks that every React developer should master.
1. useState - Managing Component State
The useState
hook is your go-to solution for managing local component state in functional components.
Basic Usage
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}
Advanced Patterns
Functional Updates: When the new state depends on the previous state, use functional updates to avoid stale closures:
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount((prevCount) => prevCount + 1);
};
const incrementTwice = () => {
setCount((prevCount) => prevCount + 1);
setCount((prevCount) => prevCount + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={incrementTwice}>+2</button>
</div>
);
}
Complex State Objects: For complex state, consider using multiple useState
calls or useReducer
:
function UserProfile() {
const [user, setUser] = useState({
name: "",
email: "",
age: 0,
});
const updateUser = (field, value) => {
setUser((prevUser) => ({
...prevUser,
[field]: value,
}));
};
return (
<form>
<input
value={user.name}
onChange={(e) => updateUser("name", e.target.value)}
placeholder="Name"
/>
<input
value={user.email}
onChange={(e) => updateUser("email", e.target.value)}
placeholder="Email"
/>
</form>
);
}
2. useEffect - Side Effects and Lifecycle
The useEffect
hook handles side effects in functional components, replacing lifecycle methods from class components.
Basic Side Effects
import React, { useState, useEffect } from "react";
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUsers = async () => {
try {
const response = await fetch("/api/users");
const userData = await response.json();
setUsers(userData);
} catch (error) {
console.error("Failed to fetch users:", error);
} finally {
setLoading(false);
}
};
fetchUsers();
}, []); // Empty dependency array means this runs once on mount
if (loading) return <div>Loading...</div>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Cleanup and Dependencies
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((prevSeconds) => prevSeconds + 1);
}, 1000);
// Cleanup function
return () => clearInterval(interval);
}, []); // Empty deps = run once, cleanup on unmount
return <div>Timer: {seconds} seconds</div>;
}
Conditional Effects
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
const searchAPI = async () => {
const response = await fetch(`/api/search?q=${query}`);
const data = await response.json();
setResults(data);
};
searchAPI();
}, [query]); // Re-run when query changes
return (
<div>
{results.map((result) => (
<div key={result.id}>{result.title}</div>
))}
</div>
);
}
3. useMemo - Performance Optimization
The useMemo
hook memoizes expensive calculations, preventing unnecessary re-computations.
Basic Usage
import React, { useState, useMemo } from "react";
function ExpensiveCalculation({ items }) {
const [multiplier, setMultiplier] = useState(1);
// Expensive calculation that we want to memoize
const expensiveValue = useMemo(() => {
console.log("Calculating expensive value...");
return items.reduce((sum, item) => sum + item.value, 0) * multiplier;
}, [items, multiplier]); // Only recalculate when items or multiplier change
return (
<div>
<p>Expensive Value: {expensiveValue}</p>
<button onClick={() => setMultiplier((m) => m + 1)}>
Increase Multiplier
</button>
</div>
);
}
Complex Object Memoization
function UserDashboard({ users, filters }) {
const filteredAndSortedUsers = useMemo(() => {
return users
.filter((user) => {
return filters.every((filter) => filter.fn(user));
})
.sort((a, b) => a.name.localeCompare(b.name));
}, [users, filters]);
return (
<div>
{filteredAndSortedUsers.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}
4. useCallback - Function Memoization
The useCallback
hook memoizes functions, preventing unnecessary re-renders of child components.
Preventing Unnecessary Re-renders
import React, { useState, useCallback } from "react";
// Child component that might re-render unnecessarily
const Button = React.memo(({ onClick, children }) => {
console.log("Button rendered");
return <button onClick={onClick}>{children}</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [otherValue, setOtherValue] = useState(0);
// Without useCallback, this function is recreated on every render
const handleClick = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []); // No dependencies, so function never changes
const handleOtherClick = useCallback(() => {
setOtherValue((prev) => prev + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<p>Other Value: {otherValue}</p>
<Button onClick={handleClick}>Increment Count</Button>
<Button onClick={handleOtherClick}>Increment Other</Button>
</div>
);
}
Event Handlers with Dependencies
function TodoList({ todos, onUpdateTodo }) {
const [filter, setFilter] = useState("all");
const handleToggle = useCallback(
(id) => {
onUpdateTodo(id, {
completed: !todos.find((t) => t.id === id)?.completed,
});
},
[todos, onUpdateTodo]
);
const filteredTodos = useMemo(() => {
return todos.filter((todo) => {
if (filter === "completed") return todo.completed;
if (filter === "active") return !todo.completed;
return true;
});
}, [todos, filter]);
return (
<div>
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="all">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
{filteredTodos.map((todo) => (
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
))}
</div>
);
}
5. Custom Hooks - Reusable Logic
Custom hooks allow you to extract and reuse stateful logic between components.
useLocalStorage Hook
import { useState, useEffect } from "react";
function useLocalStorage(key, initialValue) {
// Get value from localStorage or use initial value
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// Update localStorage when state changes
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage("theme", "light");
const [language, setLanguage] = useLocalStorage("language", "en");
return (
<div>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
<option value="en">English</option>
<option value="es">Spanish</option>
</select>
</div>
);
}
useAPI Hook
import { useState, useEffect } from "react";
function useAPI(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error } = useAPI(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
useDebounce Hook
import { useState, useEffect } from "react";
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Usage in a search component
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState("");
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const { data: results, loading } = useAPI(
debouncedSearchTerm ? `/api/search?q=${debouncedSearchTerm}` : null
);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
{loading && <div>Searching...</div>}
{results && (
<ul>
{results.map((result) => (
<li key={result.id}>{result.title}</li>
))}
</ul>
)}
</div>
);
}
Best Practices and Common Pitfalls
1. Dependency Arrays
Always include all dependencies in your dependency arrays to avoid bugs:
// ❌ Missing dependency
useEffect(() => {
fetchData(userId);
}, []); // userId should be in dependencies
// ✅ Correct
useEffect(() => {
fetchData(userId);
}, [userId]);
2. Avoid Overusing useMemo and useCallback
Only use them when you have actual performance issues:
// ❌ Unnecessary memoization
const expensiveValue = useMemo(() => {
return a + b; // Simple calculation doesn't need memoization
}, [a, b]);
// ✅ Use for actually expensive operations
const expensiveValue = useMemo(() => {
return heavyCalculation(largeDataSet);
}, [largeDataSet]);
3. Custom Hook Naming
Always start custom hooks with "use":
// ❌ Wrong naming
function localStorage(key, initialValue) {
/* ... */
}
// ✅ Correct naming
function useLocalStorage(key, initialValue) {
/* ... */
}
Conclusion
These 5 React hooks form the foundation of modern React development:
- useState - For local component state
- useEffect - For side effects and lifecycle events
- useMemo - For expensive calculations
- useCallback - For function memoization
- Custom Hooks - For reusable stateful logic
Mastering these hooks will make you a more effective React developer, enabling you to write cleaner, more performant, and more maintainable code.
Remember: Hooks are powerful tools, but use them judiciously. Not every piece of logic needs to be memoized, and not every effect needs optimization. Focus on readability first, then optimize when you have real performance issues.
Happy coding! 🚀