REACT & NEXT.JS / 2. USESTATE & USEEFFECT

useState & useEffect

State and side effects — the two most important hooks


EXPLANATION

Hooks let function components have state and lifecycle. Rules:
• Only call hooks at the top level (not inside loops, conditions, or nested functions)
• Only call hooks from React function components or custom hooks

useState: adds local state to a component.
const [value, setValue] = useState(initialValue)
• value → current state
• setValue → function to update state (triggers re-render)
• State updates are asynchronous — don't rely on state immediately after setting it

useEffect: runs side effects after render.
useEffect(() => { /* effect */ }, [dependencies])
• Empty array [] → run once on mount
• [dep1, dep2] → run when dep1 or dep2 changes
• No array → run after every render (almost never what you want)
• Return a cleanup function to prevent memory leaks

Common mistakes:
• Infinite loop: putting an object/array in deps without useMemo
• Missing dependency: eslint-plugin-react-hooks will warn you
• Not cleaning up: event listeners, subscriptions, timers

DIAGRAM

Component lifecycle with hooks:

  Mount → useEffect(fn, [])    ← runs once
     ↓
  Render → JSX returned
     ↓
  User interacts → setState()
     ↓
  Re-render → JSX returned again
     ↓
  useEffect(fn, [dep]) ← runs if dep changed
     ↓
  Unmount → cleanup function runs

  State update batching (React 18):
  handleClick() {
    setA(1)   ← doesn't re-render yet
    setB(2)   ← doesn't re-render yet
    setC(3)   ← doesn't re-render yet
  }           ← ONE re-render with all updates

CODE

TSX
1"use client"; // needed in Next.js App Router for hooks
2
3import { useState, useEffect } from "react";
4
5// ── Counter basic useState ──────────────────────────────────────
6export function Counter() {
7 const [count, setCount] = useState(0);
8
9 return (
10 <div>
11 <p>Count: {count}</p>
12 <button onClick={() => setCount(count + 1)}>+</button>
13 <button onClick={() => setCount(prev => prev - 1)}>-</button>
14 {/* prev => prev - 1: functional update always use this when
15 new state depends on old state */}
16 <button onClick={() => setCount(0)}>Reset</button>
17 </div>
18 );
19}
20
21// ── Object state always spread to preserve other fields ─────────
22export function UserForm() {
23 const [user, setUser] = useState({ name: "", email: "", age: 0 });
24
25 const updateField = (field: string, value: string | number) => {
26 setUser(prev => ({ ...prev, [field]: value })); // spread first!
27 };
28
29 return (
30 <form>
31 <input value={user.name} onChange={e => updateField("name", e.target.value)} />
32 <input value={user.email} onChange={e => updateField("email", e.target.value)} />
33 </form>
34 );
35}
36
37// ── useEffect: data fetching ──────────────────────────────────────
38export function PostList() {
39 const [posts, setPosts] = useState<Post[]>([]);
40 const [loading, setLoading] = useState(true);
41 const [error, setError] = useState<string | null>(null);
42
43 useEffect(() => {
44 let cancelled = false; // cleanup flag prevents setting state on unmounted component
45
46 async function fetchPosts() {
47 try {
48 setLoading(true);
49 const res = await fetch("/api/posts");
50 const data = await res.json();
51 if (!cancelled) setPosts(data);
52 } catch (e) {
53 if (!cancelled) setError("Failed to fetch");
54 } finally {
55 if (!cancelled) setLoading(false);
56 }
57 }
58
59 fetchPosts();
60 return () => { cancelled = true; }; // cleanup
61 }, []); // [] = run once on mount
62
63 if (loading) return <div>Loading...</div>;
64 if (error) return <div>Error: {error}</div>;
65 return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
66}
67
68// ── useEffect: event listener with cleanup ────────────────────────
69export function WindowWidth() {
70 const [width, setWidth] = useState(window.innerWidth);
71
72 useEffect(() => {
73 const handler = () => setWidth(window.innerWidth);
74 window.addEventListener("resize", handler);
75 return () => window.removeEventListener("resize", handler); // cleanup!
76 }, []);
77
78 return <p>Window width: {width}px</p>;
79}
80
81type Post = { id: number; title: string };
← PREV1. Components & JSXNEXT →3. useRef, useMemo, useCallback