Hooks let React function components hold state and react to changes. Almost every React component you write uses useState, and most data-driven ones use useEffect.
useState: remember a value between renders
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}useState(0)sets the initial value.- It returns the current value and a setter function.
- Calling the setter re-renders the component with the new value.
Update based on the previous value
When the new value depends on the old one, pass a function. This is safe even when several updates happen quickly.
setCount((prev) => prev + 1);State with objects: always create a new object
const [form, setForm] = useState({ name: "", email: "" });
function handleChange(e) {
setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
}
return (
<form>
<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />
</form>
);Never mutate state directly (form.name = "Ravi"). React compares references, so a mutated object may not trigger a re-render.
useEffect: sync with things outside React
Use useEffect for side effects: fetching data, subscriptions, timers, or updating the document title.
import { useEffect, useState } from "react";
function CourseList() {
const [courses, setCourses] = useState([]);
const [error, setError] = useState(null);
useEffect(() => {
let ignore = false;
fetch("/api/courses")
.then((res) => {
if (!res.ok) throw new Error("Failed to load");
return res.json();
})
.then((data) => { if (!ignore) setCourses(data); })
.catch((err) => { if (!ignore) setError(err.message); });
return () => { ignore = true; }; // cleanup
}, []);
if (error) return <p>{error}</p>;
return <ul>{courses.map((c) => <li key={c.id}>{c.title}</li>)}</ul>;
}The ignore flag prevents setting state after the component unmounts or after a newer request starts.
The dependency array
| Code | When the effect runs |
|---|---|
useEffect(fn) | After every render |
useEffect(fn, []) | Once, after the first render |
useEffect(fn, [query]) | After the first render and whenever query changes |
Search that refetches when the query changes
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${encodeURIComponent(query)}`, { signal: controller.signal })
.then((r) => r.json())
.then(setResults)
.catch(() => {});
return () => controller.abort();
}, [query]);Cleanup
Return a function from the effect to undo what it set up. React runs it before the effect runs again and when the component unmounts.
useEffect(() => {
const id = setInterval(() => setSeconds((s) => s + 1), 1000);
return () => clearInterval(id);
}, []);Common mistakes
- Missing dependencies: if the effect uses a value, list it in the array. The
react-hooks/exhaustive-depslint rule catches this. - Infinite loops: setting state inside an effect that depends on that same state, with no condition.
- Using an effect for derived data: if a value can be calculated from props or state, calculate it during render instead of storing it in state.
- Calling hooks conditionally: hooks must be called in the same order on every render, so never call them inside
ifstatements or loops.
Interview questions
- Why can't hooks be called inside conditions? React tracks hooks by call order; changing the order between renders mixes up their state.
- What is the difference between props and state? Props are passed in by the parent and are read-only; state is owned and updated by the component.
- When does a component re-render? When its state changes, when its parent re-renders, or when a context it uses changes.
Next steps
Build a small "course search" app with a search box, loading state and error state. Learn Next.js, TypeScript and AI-assisted UI development in the Frontend Development + AI course.
