React Hooks Explained: useState, useEffect, useContext with Real Examples
Clear explanation of React hooks for beginners — useState, useEffect, useContext, useCallback, useMemo. With real code examples and common mistakes to avoid.
React Hooks are the feature that made React the dominant frontend framework. Before hooks (pre-2019), managing state and side effects required class components with lifecycle methods. Today, every modern React app uses functional components with hooks. This guide covers the hooks that appear in every fresher interview and every production app.
What Are Hooks?
Hooks are functions that let you "hook into" React features — state, lifecycle, context — from inside functional components. They always start with use.
Rules of Hooks (memorize these — they come up in interviews):
- Only call hooks at the top level of a component — not inside loops, conditions, or nested functions
- Only call hooks from React functional components or custom hooks — not regular JavaScript functions
useState — Managing Local State
useState is the most fundamental hook. It creates a piece of state within a component.
const [value, setValue] = useState(initialValue);
Basic counter:
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>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}
Managing an object in state:
function ProfileForm() {
const [form, setForm] = useState({ name: "", email: "", city: "" });
function handleChange(field, value) {
setForm({ ...form, [field]: value }); // spread existing, override the changed field
}
return (
<form>
<input value={form.name} onChange={(e) => handleChange("name", e.target.value)} />
<input value={form.email} onChange={(e) => handleChange("email", e.target.value)} />
</form>
);
}
Critical mistake: Never mutate state directly.
// Wrong — React won't detect the change and won't re-render
state.name = "New Name";
// Correct — always create a new object/array
setForm({ ...form, name: "New Name" });
setItems([...items, newItem]);
useEffect — Side Effects After Render
useEffect runs code after React renders the component. Use it for API calls, subscriptions, timers, and DOM manipulation.
useEffect(() => {
// side effect code here
return () => {
// cleanup code here (runs before next effect or on unmount)
};
}, [dependencies]);
Fetch data on mount:
function StudentList() {
const [students, setStudents] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch("/api/students")
.then((res) => {
if (!res.ok) throw new Error("Failed to fetch");
return res.json();
})
.then((data) => {
setStudents(data);
setLoading(false);
})
.catch((err) => {
setError(err.message);
setLoading(false);
});
}, []); // empty array = run once on mount
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{students.map((s) => <li key={s.id}>{s.name}</li>)}
</ul>
);
}
Re-fetch when a value changes:
function StudentsByCity({ city }) {
const [students, setStudents] = useState([]);
useEffect(() => {
fetch(`/api/students?city=${city}`)
.then(res => res.json())
.then(setStudents);
}, [city]); // re-runs whenever `city` prop changes
return <ul>{students.map(s => <li key={s.id}>{s.name}</li>)}</ul>;
}
Effect with cleanup (important for interviews):
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
return () => clearInterval(interval); // cleanup: clears the interval on unmount
}, []);
return <p>Running for {seconds} seconds</p>;
}
useContext — Sharing State Without Prop Drilling
Prop drilling is when you pass props through many intermediate components that don't need the data — just to get it to a deeply nested child. useContext solves this.
Create and use a theme context:
import { createContext, useContext, useState } from "react";
// 1. Create the context
const ThemeContext = createContext("light");
// 2. Provide it at the top level
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Header />
<Main />
</ThemeContext.Provider>
);
}
// 3. Consume it anywhere in the tree — no prop drilling
function Header() {
const { theme, setTheme } = useContext(ThemeContext);
return (
<header style={{ background: theme === "dark" ? "#222" : "#fff" }}>
<button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
Toggle Theme
</button>
</header>
);
}
useCallback — Memoizing Functions
useCallback returns a memoized version of a callback that only changes if one of its dependencies changes. Useful when passing functions as props to child components to prevent unnecessary re-renders.
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
// Without useCallback: this function is re-created on every render
// With useCallback: only re-created when count changes
const handleIncrement = useCallback(() => {
setCount(prev => prev + 1);
}, []); // no dependencies — function never changes
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<ChildButton onClick={handleIncrement} />
</div>
);
}
useMemo — Memoizing Computed Values
useMemo caches the result of a computation. Use it when you have expensive calculations that shouldn't re-run on every render.
function StudentStats({ students }) {
// Without useMemo: recalculates on every render, even when students hasn't changed
// With useMemo: only recalculates when students changes
const stats = useMemo(() => {
const total = students.length;
const placed = students.filter(s => s.isPlaced).length;
const avgSalary = students.reduce((sum, s) => sum + s.salary, 0) / total;
return { total, placed, avgSalary };
}, [students]);
return (
<div>
<p>Total: {stats.total}</p>
<p>Placed: {stats.placed}</p>
<p>Avg Salary: ₹{stats.avgSalary.toFixed(1)} LPA</p>
</div>
);
}
useRef — Accessing DOM Elements
useRef holds a mutable reference that doesn't trigger re-renders when changed. Most commonly used to access DOM elements directly.
function SearchInput() {
const inputRef = useRef(null);
function focusInput() {
inputRef.current.focus(); // directly access the DOM element
}
return (
<div>
<input ref={inputRef} type="text" placeholder="Search..." />
<button onClick={focusInput}>Focus</button>
</div>
);
}
Common Hook Mistakes in Interviews
1. Calling hooks inside conditions:
// Wrong
if (isLoggedIn) {
const [user, setUser] = useState(null); // ❌ violates rules of hooks
}
// Correct
const [user, setUser] = useState(null); // always at top level
2. Missing dependencies in useEffect:
// Wrong — effect uses `userId` but doesn't list it as dependency
useEffect(() => {
fetchUser(userId);
}, []); // ❌ ESLint will warn about this
// Correct
useEffect(() => {
fetchUser(userId);
}, [userId]); // ✓
3. Directly mutating state:
// Wrong
const newList = students;
newList.push(newStudent);
setStudents(newList); // ❌ same reference, React won't re-render
// Correct
setStudents([...students, newStudent]); // ✓ new array
4. Infinite loop in useEffect:
// Wrong — object/array created inline is a new reference each render
useEffect(() => {
fetchData(options);
}, [{ page: 1 }]); // ❌ new object every render → infinite loop
// Correct — primitive values, or memoize the object
useEffect(() => {
fetchData(options);
}, [page]); // ✓
These hooks are tested in 90% of React fresher interviews. Knowing the "why" behind each one — not just the syntax — is what separates candidates who get offers from those who don't. CodeBegun's program covers React from fundamentals to production patterns as part of the Java Full Stack curriculum. Learn more →
Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaching the next generation to code the way the industry actually works.
