Mastering React Hooks: A Complete Guide
Learn how to use React hooks effectively to build better components and manage state.
Mastering React Hooks: A Complete Guide
React Hooks have revolutionized how we write React components. They provide a way to use state and other React features in functional components. In this comprehensive guide, we'll explore all the built-in hooks and learn how to create custom hooks.
What are React Hooks?
Hooks are functions that allow you to "hook into" React state and lifecycle features from function components. They were introduced in React 16.8 and have become the standard way to write React components.
Built-in Hooks
1. useState
The most basic hook for managing state:
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>
);
}
2. useEffect
For side effects in functional components:
import { useEffect, useState } from "react";
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchUser() {
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>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
3. useContext
For consuming React context:
import { createContext, useContext, useState } from "react";
// Create context
const ThemeContext = createContext<{
theme: string;
toggleTheme: () => void;
}>({
theme: "light",
toggleTheme: () => {},
});
// Provider component
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Hook to use theme
function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}
// Usage
function App() {
return (
<ThemeProvider>
<Header />
<Main />
</ThemeProvider>
);
}
function Header() {
const { theme, toggleTheme } = useTheme();
return (
<header className={theme}>
<button onClick={toggleTheme}>
Toggle {theme === "light" ? "Dark" : "Light"} Mode
</button>
</header>
);
}
4. useReducer
For complex state logic:
import { useReducer } from "react";
type State = {
count: number;
step: number;
};
type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "setStep"; payload: number };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment":
return { ...state, count: state.count + state.step };
case "decrement":
return { ...state, count: state.count - state.step };
case "setStep":
return { ...state, step: action.payload };
default:
return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, {
count: 0,
step: 1,
});
return (
<div>
<p>Count: {state.count}</p>
<p>Step: {state.step}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<input
type="number"
value={state.step}
onChange={(e) =>
dispatch({
type: "setStep",
payload: parseInt(e.target.value) || 1,
})
}
/>
</div>
);
}
Custom Hooks
Custom hooks allow you to extract component logic into reusable functions:
1. useLocalStorage
import { useState, useEffect } from "react";
function useLocalStorage<T>(key: string, initialValue: T) {
// Get from local storage then parse stored json or return initialValue
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 reading localStorage key "${key}":`, error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that persists the new value to localStorage
const setValue = (value: T | ((val: T) => T)) => {
try {
// Allow value to be a function so we have the same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
return [storedValue, setValue] as const;
}
// Usage
function App() {
const [name, setName] = useLocalStorage("name", "John");
return (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
</div>
);
}
2. useDebounce
import { useState, useEffect } from "react";
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;
}
// Usage
function SearchInput() {
const [searchTerm, setSearchTerm] = useState("");
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
// Perform search
console.log("Searching for:", debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}
3. useAsync
import { useState, useEffect } from "react";
interface AsyncState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useAsync<T>(
asyncFn: () => Promise<T>,
deps: any[] = []
): AsyncState<T> {
const [state, setState] = useState<AsyncState<T>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
let mounted = true;
async function execute() {
try {
setState({ data: null, loading: true, error: null });
const result = await asyncFn();
if (mounted) {
setState({ data: result, loading: false, error: null });
}
} catch (error) {
if (mounted) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error("Unknown error"),
});
}
}
}
execute();
return () => {
mounted = false;
};
}, deps);
return state;
}
// Usage
function UserList() {
const {
data: users,
loading,
error,
} = useAsync(() => fetch("/api/users").then((res) => res.json()), []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!users) return <div>No users found</div>;
return (
<ul>
{users.map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Best Practices
1. Follow the Rules of Hooks
- Only call hooks at the top level
- Only call hooks from React functions
- Always include dependencies in useEffect
2. Use Custom Hooks for Reusable Logic
Extract common patterns into custom hooks to avoid code duplication.
3. Optimize with useMemo and useCallback
import { useMemo, useCallback } from "react";
function ExpensiveComponent({
items,
filter,
}: {
items: any[];
filter: string;
}) {
// Memoize expensive calculations
const filteredItems = useMemo(() => {
return items.filter((item) => item.name.includes(filter));
}, [items, filter]);
// Memoize callback functions
const handleItemClick = useCallback((itemId: string) => {
console.log("Item clicked:", itemId);
}, []);
return (
<div>
{filteredItems.map((item) => (
<div key={item.id} onClick={() => handleItemClick(item.id)}>
{item.name}
</div>
))}
</div>
);
}
Conclusion
React Hooks have made functional components more powerful and easier to work with. By understanding the built-in hooks and creating custom hooks, you can write cleaner, more maintainable React code.
Remember to:
- Use hooks consistently
- Extract reusable logic into custom hooks
- Follow the rules of hooks
- Optimize performance when necessary
Resources
Happy coding! 🚀
Project Links
Related Posts
CSS Grid vs Flexbox: When to Use Each
A comprehensive comparison of CSS Grid and Flexbox, with practical examples and use cases.
Getting Started with Next.js 15
Learn how to build modern web applications with Next.js 15 and the App Router.
TypeScript Best Practices for 2024
Learn the most effective TypeScript patterns and practices for building robust applications.