If you want to write modern React applications and you want companies to
Hire React.js developers like you, then mastering Hooks is not optional
anymore. Hooks are the foundation of every React codebase written after
2019.They replaced class components, simplified state management, and
made code reuse actually practical. This guide covers every major Hook
with real working code, clear explanations, and common mistakes to avoid.
What Are React Hooks?
Hooks are functions that let you use React features like state and
lifecycle inside regular function components. Before Hooks existed,
you had to write class components to do any of that.
Here is the same Counter component, written both ways:
Old way using a Class Component:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={() =>
this.setState({ count: this.state.count + 1 })
}>
Add
</button>
</div>
);
}
}
Modern way using a Hook:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Add</button>
</div>
);
}
Same result. Much less code. Far easier to read and test.
All the Hooks Covered in This Guide
| Hook | What It Does |
|---|---|
| useState | Adds state to a component |
| useEffect | Runs side effects after render |
| useContext | Reads shared data without prop drilling |
| useRef | Accesses DOM elements or stores mutable values |
| useMemo | Caches expensive calculations |
| useCallback | Caches a function reference |
| useReducer | Manages complex state with actions |
| Custom Hooks | Reuses logic across multiple components |
1. useState
useState gives your component memory. It stores a value and
triggers a re-render whenever that value changes.
Syntax:
const [value, setValue] = useState(initialValue);
Real example with a todo list:
import { useState } from "react";
function TodoApp() {
const [task, setTask] = useState("");
const [todos, setTodos] = useState([]);
function addTodo() {
if (!task.trim()) return;
setTodos([...todos, task]);
setTask("");
}
return (
<div>
<input
value={task}
onChange={(e) => setTask(e.target.value)}
placeholder="Type a task"
/>
<button onClick={addTodo}>Add</button>
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
</div>
);
}
Most common mistake: Mutating state directly instead of using the
setter function.
// Wrong
todos.push("new task");
// Right
setTodos([...todos, "new task"]);
2. useEffect
useEffect runs code after the component renders. You use it for API
calls, timers, subscriptions, and updating the document title.
Syntax:
useEffect(() => {
// side effect code
return () => {
// cleanup code (optional)
};
}, [dependencies]);
Real example fetching users from an API:
import { useState, useEffect } from "react";
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => res.json())
.then((data) => {
setUsers(data);
setLoading(false);
});
}, []);
if (loading) return <p>Loading...</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
How the dependency array controls when the effect runs:
useEffect(() => { ... }); // Runs after every render
useEffect(() => { ... }, []); // Runs once on mount only
useEffect(() => { ... }, [userId]); // Runs when userId changes
Real example with cleanup to prevent memory leaks:
useEffect(() => {
const timer = setInterval(() => {
console.log("tick");
}, 1000);
return () => clearInterval(timer);
}, []);
Always return a cleanup function when you set up timers,
subscriptions, or event listeners.
3. useContext
Prop drilling means passing data through many layers of components
just to reach the one that actually needs it. useContext solves
this by letting any component read shared data directly.
Step 1: Create the context and a provider:
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
Step 2: Wrap your app:
// main.jsx
<ThemeProvider>
<App />
</ThemeProvider>
Step 3: Read the context from any component:
function Navbar() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<nav style={{ background: theme === "dark" ? "#222" : "#fff" }}>
<span>My App</span>
<button onClick={() =>
setTheme(theme === "dark" ? "light" : "dark")
}>
Toggle Theme
</button>
</nav>
);
}
No props passed through intermediate components. The data is
available anywhere inside the provider.
4. useRef
useRef has two distinct uses that are worth understanding separately.
Use 1: Access a DOM element directly
import { useRef, useEffect } from "react";
function AutoFocusInput() {
const inputRef = useRef(null);
useEffect(() => {
inputRef.current.focus();
}, []);
return <input ref={inputRef} placeholder="This auto-focuses" />;
}
Use 2: Store a value that does not trigger a re-render
function StopwatchApp() {
const [seconds, setSeconds] = useState(0);
const intervalRef = useRef(null);
function start() {
intervalRef.current = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
}
function stop() {
clearInterval(intervalRef.current);
}
return (
<div>
<p>{seconds}s</p>
<button onClick={start}>Start</button>
<button onClick={stop}>Stop</button>
</div>
);
}
The key difference: changing a useRef value does not cause the
component to re-render. That is what makes it the right tool for
storing timers and interval IDs.
5. useMemo
useMemo caches the result of an expensive calculation so it only
runs again when its dependencies change, not on every render.
import { useState, useMemo } from "react";
function ProductList({ products, filter }) {
const filteredProducts = useMemo(() => {
return products.filter((p) =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
}, [products, filter]);
return (
<ul>
{filteredProducts.map((p) => (
<li key={p.id}>{p.name} - {p.price}</li>
))}
</ul>
);
}
The filter calculation only runs when products or filter changes,
not when unrelated state changes cause a re-render.
Do not add useMemo everywhere. Only use it when you have measured
a real performance issue. Adding it unnecessarily makes code harder
to read for no actual benefit.
6. useCallback
useCallback caches a function so that its reference stays the same
between renders. This matters when you pass functions as props to
components wrapped in React.memo.
import { useState, useCallback, memo } from "react";
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
const handleClick = useCallback(() => {
console.log("Button clicked");
}, []);
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<ChildButton onClick={handleClick} />
</div>
);
}
const ChildButton = memo(({ onClick }) => {
console.log("Child rendered");
return <button onClick={onClick}>Click Me</button>;
});
Without useCallback, every re-render of Parent creates a new
function reference. ChildButton would re-render every time even
though nothing about it actually changed.
7. useReducer
When state logic becomes complex, especially when one action needs
to update multiple values at once, useReducer is cleaner than
multiple useState calls.
import { useReducer } from "react";
const initialState = { count: 0, step: 1 };
function reducer(state, action) {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + state.step };
case "DECREMENT":
return { ...state, count: state.count - state.step };
case "RESET":
return initialState;
case "SET_STEP":
return { ...state, step: action.payload };
default:
return state;
}
}
function AdvancedCounter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count} | Step: {state.step}</p>
<button onClick={() => dispatch({ type: "DECREMENT" })}>-</button>
<button onClick={() => dispatch({ type: "INCREMENT" })}>+</button>
<button onClick={() => dispatch({ type: "RESET" })}>Reset</button>
<input
type="number"
value={state.step}
onChange={(e) =>
dispatch({ type: "SET_STEP", payload: Number(e.target.value) })
}
/>
</div>
);
}
The reducer function is a pure function that takes current state and
an action, and returns the next state. This pattern makes complex
state changes predictable and easy to test.
8. Custom Hooks
This is where Hooks become truly powerful. A custom hook is a regular
JavaScript function whose name starts with use and that calls other
hooks inside it. You use custom hooks to extract and reuse logic across
multiple components.
Custom Hook: useFetch
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
fetch(url)
.then((res) => {
if (!res.ok) throw new Error("Request failed");
return res.json();
})
.then((json) => {
setData(json);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
Using the custom hook in multiple components:
function PostList() {
const { data, loading, error } =
useFetch("https://jsonplaceholder.typicode.com/posts");
if (loading) return <p>Loading posts...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{data.slice(0, 5).map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
function UserCount() {
const { data, loading } =
useFetch("https://jsonplaceholder.typicode.com/users");
if (loading) return <p>Loading...</p>;
return <p>Total users: {data?.length}</p>;
}
Two different components. Same fetch logic. Zero code duplication.
This is what makes custom hooks one of the most valuable patterns
in React development.
Rules of Hooks
These two rules are enforced by React and breaking them will cause bugs
that are difficult to trace.
Rule 1: Only call Hooks at the top level of a component.
Never inside if statements, loops, or nested functions.
Rule 2: Only call Hooks inside React function components
or other custom hooks.
Wrong:
function Bad({ isLoggedIn }) {
if (isLoggedIn) {
const [data, setData] = useState(null); // breaks Rule 1
}
}
Right:
function Good({ isLoggedIn }) {
const [data, setData] = useState(null); // always at the top level
if (!isLoggedIn) return null;
return <div>{data}</div>;
}
Quick Reference
useState Store and update values, triggers re-render
useEffect Side effects: API calls, timers, subscriptions
useContext Read shared global data without prop drilling
useRef DOM access or persistent values without re-render
useMemo Cache expensive calculated values
useCallback Cache function references for child components
useReducer Complex state with multiple related values
Custom Hook Extract and reuse any logic across components
Projects to Build and Practice
The fastest way to learn Hooks is to use them in real projects. Here
are good starting points matched to the hooks they teach:
- Todo App: useState, useReducer
- Weather App: useEffect, custom useFetch hook
- Dark Mode Toggle: useContext, useState
- Search with Filter: useMemo, useState
- Click-outside Detector: useRef, useEffect
- Shopping Cart: useReducer, useContext combined
Pick one, build it from scratch, and you will understand Hooks
at a level that reading alone cannot give you.
Final Thoughts
Most developers learn Hooks by memorizing the API. That approach works
until something breaks and you cannot explain why. The better approach
is to understand what each Hook is solving. useState solves the problem
of memory inside a function. useEffect solves the problem of running
code in response to changes. useRef solves the problem of holding a
value without triggering a render.
When you understand the problem each Hook exists to solve, picking the
right one for any situation becomes straightforward.
Start with useState and useEffect. Build something real. Add the other
hooks only when you actually need them.
Found this useful? Drop a reaction and follow for weekly React posts,
project walkthroughs, and practical guides.