React Hooks Explained — Complete Guide with Examples & Best Practices
React hooks are functions that let you use state and effects in function components - like a utility belt giving your components memory without writing a class.
What You’ll Learn
- What hooks are and why they replaced class components
- The Rules of Hooks (and why they exist)
- Every built-in hook:
useState,useEffect,useContext,useRef,useReducer,useMemo,useCallback - How to create reusable custom hooks
- Real-world patterns used in production apps
Why React Hooks Matters
Hooks are how modern React apps are built — every component you write will likely use at least one. At DodaTech, hooks power the Durga Antivirus Pro dashboard (real-time threat updates with useEffect, scan queue state with useReducer) and the Doda Browser extension (tab management state, keyboard shortcut hooks). Without hooks, you’d write more code, repeat yourself, and deal with confusing this binding in class components.
Your Learning Path
flowchart LR
A[React Basics<br/>Components & JSX] --> B[React Hooks]
B --> C[React Advanced<br/>Patterns]
B --> D[React Reference]
B --> E[Next.js]
style B fill:#3b82f6,color:#fff,stroke:#2563eb,stroke-width:3px
What Are Hooks?
Before React 16.8 (2019), state and lifecycle features were only available in class components:
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return <p>{this.state.count}</p>;
}
}This was verbose and confusing (this, super, constructor). Hooks solve this by letting you “hook into” React features from function components — no classes needed.
Analogy: Think of a component as a kitchen appliance. Without hooks, each appliance had its own unique control panel (class lifecycle methods). With hooks, every appliance gets a standard utility belt — you just pick the tools you need.
Rules of Hooks
- Call hooks at the top level — not inside loops, conditions, or nested functions
- Call hooks from React function components or custom hooks only
- Consistent order — the order of hook calls must be the same between renders
function MyComponent() {
const [count, setCount] = useState(0); // ✅ Top level
useEffect(() => { document.title = `${count}`; }, [count]);
}
function Bad({ feature }) {
if (feature) {
const [data, setData] = useState(null); // ❌ Inside condition
}
}Why this rule? React relies on the order of hooks to associate each call with its internal state. If you conditionally skip a hook, the order shifts and React returns the wrong state.
useState — Local Component Memory
The simplest hook. It lets your component remember a value between renders:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(prev => prev + 1)}>+</button>
<button onClick={() => setCount(prev => prev - 1)}>-</button>
</div>
);
}Line by line:
const [count, setCount] = useState(0);—useState(0)returns[0, setterFunction]. We destructure them intocountandsetCount.setCount(prev => prev + 1)— The functional form passes the previous state as an argument. This guarantees you update from the latest value even with multiple rapid updates.- Every
setCountcall triggers a re-render: React calls the component again, anduseStatereturns the current stored value.
State with Objects
function UserProfile() {
const [user, setUser] = useState({ name: "", email: "" });
const updateName = (name) => {
setUser(prev => ({ ...prev, name })); // Merge with spread
};
}State updates replace, they don’t merge. Use the spread operator (...prev) to copy existing properties before overriding.
useEffect — Side Effect Manager
useEffect lets you perform side effects — anything that interacts with the outside world: API calls, timers, event listeners, browser storage.
import { useState, useEffect } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => { setUser(data); setLoading(false); });
}, [userId]);
if (loading) return <p>Loading...</p>;
return <h2>{user.name}</h2>;
}Line by line:
useEffect(() => { ... }, [userId]);— First argument is the effect function. The dependency array[userId]tells React: “Only re-run whenuserIdchanges.”- Inside: fetch data, parse JSON, call
setUser/setLoading - On first render, the effect runs. If
userIdchanges, it runs again. Otherwise, it never re-runs — preventing unnecessary API calls.
Effect Cleanup
Some effects need cleanup — removing event listeners, clearing timers:
useEffect(() => {
const handler = (e) => setPosition({ x: e.clientX, y: e.clientY });
window.addEventListener("mousemove", handler);
return () => window.removeEventListener("mousemove", handler);
}, []);Why cleanup matters: Without it, every time the effect runs (or the component unmounts), a new listener is added but the old one is never removed — causing memory leaks.
Dependency Array Patterns
| Array | When it runs | Use case |
|---|---|---|
[] (empty) | On mount only, cleanup on unmount | Event listeners, analytics |
[dep] | On mount + when dep changes | Fetching data based on a prop |
| omitted | On every render | Rarely useful; usually a mistake |
useContext — Avoid Prop Drilling
Prop drilling is passing a prop through multiple component levels just so a deeply nested child can use it. Context solves this by making a value available to any component in the tree.
import { createContext, useContext, useState } from "react";
const ThemeContext = createContext("light");
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
const { theme, setTheme } = useContext(ThemeContext);
return <p>Current theme: {theme}</p>;
}Step by step:
createContext("light")— Creates a context with default value"light"<ThemeContext.Provider value={...}>— Wraps the tree and provides the valueuseContext(ThemeContext)— Any descendant reads the context value- When
setThemeis called in the provider, all consumers re-render
useRef — Mutable Values Without Re-renders
useRef gives you a mutable object with a .current property that persists across renders. Unlike state, changing a ref does not trigger a re-render.
function TextInput() {
const inputRef = useRef(null);
const focusInput = () => inputRef.current.focus();
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</>
);
}useRef vs useState
| Feature | useRef | useState |
|---|---|---|
| Triggers re-render? | No | Yes |
| Persists across renders? | Yes | Yes |
| Use case | DOM access, intervals, mutable values | Data that affects the UI |
useReducer — Complex State Logic
When state logic is complex (multiple sub-values, interdependent updates), useReducer centralizes it:
function reducer(state, action) {
switch (action.type) {
case "increment": return { count: state.count + 1 };
case "decrement": return { count: state.count - 1 };
case "reset": return { count: action.payload ?? 0 };
default: return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "reset", payload: 100 })}>Reset</button>
</>
);
}Line by line:
function reducer(state, action)— A pure function: takes current state and an action, returns the next state. No side effects.dispatch({ type: "increment" })— Sends a message to the reducer. Convention: object withtypeand optionalpayload.useReducer(reducer, { count: 0 })— Takes the reducer and initial state, returns[state, dispatch].
useState vs useReducer
| Feature | useState | useReducer |
|---|---|---|
| Best for | Simple values (numbers, strings) | Complex objects, multiple sub-values |
| Update logic | Inline in event handlers | Centralized in reducer function |
| Testing | Harder to test inline logic | Reducer is a pure function — easy to test |
useMemo & useCallback — Performance Optimizations
useMemo caches a computed value. useCallback caches a function reference. Both only recompute when dependencies change.
import { useMemo, useCallback, memo } from "react";
function ExpensiveList({ items, filter }) {
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
const handleDelete = useCallback((id) => {
console.log(`Delete ${id}`);
}, []);
return <ul>{filteredItems.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}useMemo vs useCallback
| Feature | useMemo | useCallback |
|---|---|---|
| Returns | Cached value | Cached function |
| Syntax | useMemo(() => value, deps) | useCallback(fn, deps) |
| Equivalent | — | useMemo(() => fn, deps) |
| Use when | Expensive computation | Passing stable fn to memoized children |
When to use them: Only when you have a measurable performance problem. They add overhead (comparison logic, memory) and make code harder to read.
Custom Hooks — Your Own Utility Functions
A custom hook is a JavaScript function that starts with use and calls other hooks. It extracts reusable logic into a single function.
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(res => res.json())
.then(data => { if (!cancelled) { setData(data); setLoading(false); }})
.catch(err => { if (!cancelled) { setError(err); setLoading(false); }});
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
function MyComponent() {
const { data, loading } = useFetch("https://api.example.com/users");
if (loading) return <p>Loading...</p>;
return <p>Users: {data?.length}</p>;
}Key insight: cancelled = true inside the cleanup function prevents setting state on an unmounted component — fixing the “Can’t perform a React state update on an unmounted component” warning.
Common Mistakes
1. Missing dependencies in useEffect
useEffect(() => { fetch(`/api/users/${userId}`); }, []); // Missing userIdFix: Include all reactive values. Use the exhaustive-deps ESLint rule to catch this.
2. Creating infinite loops
useEffect(() => { setCount(count + 1); }); // No dep array = loopFix: Always specify a dependency array.
3. Stale closures in useCallback
const logCount = useCallback(() => { console.log(count); }, []);logCount always logs the initial count because the function captured count at creation time. Fix: Add count to the dependency array.
4. Forgetting cleanup in useEffect
Subscriptions, timers, and event listeners left uncleaned cause memory leaks. Always return a cleanup function for persistent listeners.
5. Premature optimization with useMemo/useCallback
Only wrap when you have a measurable performance problem. They add overhead.
6. Calling hooks conditionally
if (user) { const [name, setName] = useState(""); } // Order shifts!7. Stale state in setInterval
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000); // Always 0
}, []);Fix: Use setCount(prev => prev + 1) (functional form).
Practice Questions
Why were hooks introduced in React 16.8?
To solve problems with class components: hard-to-reuse logic, confusing
thisbinding, and complex lifecycle methods. Hooks let function components use state and effects without classes.What happens if you call a hook inside a condition?
React throws an error because it relies on hook call order to associate state with component instances.
What is the difference between
useEffectanduseLayoutEffect?useEffectfires asynchronously after the browser paints.useLayoutEffectfires synchronously before paint. UseuseLayoutEffectonly when you need to measure layout.When should you use
useReduceroveruseState?When state logic is complex (multiple sub-values, interdependent updates) or when you want a testable reducer function.
What is a stale closure?
A callback capturing an outdated variable value. Avoid it by using the functional form of state setters or including all dependencies in the array.
Challenge
Create a useLocalStorage(key, initialValue) custom hook that persists state in localStorage and syncs across browser tabs using the storage event.
FAQ
Try It Yourself
Edit this complete React hooks sandbox — see useState, useEffect, and custom hooks in action:
What’s Next
Now that you’ve mastered hooks, dive into advanced patterns:
| Lesson | Description |
|---|---|
| https://tutorials.dodatech.com/frameworks/react/react-advanced/ | Error boundaries, portals, code splitting |
| https://tutorials.dodatech.com/frameworks/react/reference/ | Complete cheatsheet for all hooks and APIs |
| TypeScript for React | Type-safe hooks and components |
| Node.js Backend for React | Build APIs your React frontend consumes |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
What’s Next
Congratulations on completing this React Hooks tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro