Why Hooks Exist
Before Hooks (pre-2019), stateful logic in React required class components — which came with confusing this binding, verbose lifecycle methods, and difficulty reusing stateful logic between components. Hooks let function components do everything class components could do, with cleaner syntax and better composability. The React team no longer recommends class components for new code.
useState: Managing Local State
useState is the most fundamental Hook. It adds state to a function component and returns the current value plus a setter function.
import { useState } from "react";
function Counter() {
// [currentValue, setterFunction] = useState(initialValue)
const [count, setCount] = useState(0);
const [user, setUser] = useState(null);
const [items, setItems] = useState([]);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
// Updating state based on previous value — always use function form
setCount(prev => prev + 1); // ✅ safe for async updates
// Updating objects — spread the existing state
setUser(prev => ({ ...prev, name: "New Name" }));
// Updating arrays — create new array
setItems(prev => [...prev, newItem]);
useEffect: Side Effects and Lifecycle
useEffect runs code in response to renders. It replaces componentDidMount, componentDidUpdate, and componentWillUnmount from class components.
import { useState, useEffect } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// This runs after every render where userId changes
setLoading(true);
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(data => {
setUser(data);
setLoading(false);
});
// Cleanup function — runs before next effect or on unmount
return () => {
// Cancel requests, clear timers, remove listeners
};
}, [userId]); // Dependency array — effect re-runs when userId changes
}
// Common dependency array patterns:
useEffect(() => { ... }); // ❌ Runs after EVERY render (usually a bug)
useEffect(() => { ... }, []); // ✅ Runs ONCE on mount only
useEffect(() => { ... }, [id]); // ✅ Runs when id changes
useEffect(() => { ... }, [a, b]); // ✅ Runs when a OR b changes
useContext, useRef, and useReducer
useContext — Avoid Prop Drilling
const ThemeContext = createContext("light");
function App() {
return (
<ThemeContext.Provider value="dark">
<Page /> {/* Page doesn't need to pass theme down */}
</ThemeContext.Provider>
);
}
function DeepChild() {
const theme = useContext(ThemeContext); // "dark" — no prop needed
}
useRef — Persist Values Without Re-rendering
function SearchInput() {
const inputRef = useRef(null);
// Access DOM element directly
const focusInput = () => inputRef.current.focus();
// Store mutable values that don't trigger re-render
const renderCount = useRef(0);
renderCount.current += 1; // doesn't cause re-render
return <input ref={inputRef} />;
}
useReducer — Complex State Logic
const initialState = { count: 0, loading: false, error: null };
function reducer(state, action) {
switch (action.type) {
case "INCREMENT": return { ...state, count: state.count + 1 };
case "SET_LOADING": return { ...state, loading: action.payload };
default: return state;
}
}
function Component() {
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: "INCREMENT" });
}
Custom Hooks: Reusable Logic
Custom Hooks let you extract component logic into reusable functions. A custom hook is any function that starts with use and calls other Hooks.
// Custom hook that fetches data
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then(r => r.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// Using the custom hook — completely reusable
function UserProfile({ id }) {
const { data: user, loading, error } = useFetch(`/api/users/${id}`);
if (loading) return <Spinner />;
if (error) return <Error />;
return <div>{user.name}</div>;
}