React Hooks Complete Guide: useState, useEffect & More
1. What Are React Hooks?
React Hooks are functions that let you use React features in functional components. Before hooks (React 16.8), you needed class components to manage state or lifecycle methods. Hooks changed everything — they let you "hook into" React's internal mechanisms directly from a simple function.
In Next.js, hooks are especially important because they determine whether a component must be a Client Component ('use client') or can remain a Server Component.
The Golden Rule: If a component uses any hook, it must be a Client Component. Server Components cannot use hooks.
2. Why Are Hooks Needed?
Without hooks, functional components are pure functions — they receive props and return JSX, but they cannot:
- Remember values between renders (state)
- Respond to lifecycle events (mount, update, unmount)
- Access DOM elements directly
- Share logic between components cleanly
Hooks solve all of these. They let you write reusable, composable logic without classes, without this, and without confusing lifecycle methods.
In Next.js specifically:
- Server Components fetch data directly — no hooks needed
- Client Components handle interactivity — hooks power everything
- Custom hooks extract reusable logic across your app
3. Basic Syntax
Every hook follows the same pattern: import from React, call inside a component, and obey the Rules of Hooks:
- Only call hooks at the top level — not inside loops, conditions, or nested functions
- Only call hooks from React functions — components or custom hooks
1"use client"; // ← Required for any hook usage 2 3import { useState, useEffect } from "react"; 4 5export default function Example() { 6 // ✅ Top level, inside a component 7 const [count, setCount] = useState(0); 8 9 useEffect(() => { 10 console.log("Count changed:", count); 11 }, [count]); 12 13 return <div>{count}</div>; 14}
4. Simple Examples by Hook
useState — Component Memory
1"use client"; 2 3import { useState } from "react"; 4 5export function Counter() { 6 const [count, setCount] = useState(0); 7 8 return ( 9 <div className="p-6 border rounded-xl text-center"> 10 <p className="text-3xl font-bold mb-4">{count}</p> 11 <button 12 onClick={() => setCount((prev) => prev + 1)} 13 className="px-4 py-2 bg-blue-600 text-white rounded-lg" 14 > 15 Increment 16 </button> 17 </div> 18 ); 19}
When to use: Any value that changes and should trigger a re-render — form inputs, toggles, counters, selected items.
useEffect — Side Effects
1"use client"; 2 3import { useState, useEffect } from "react"; 4 5export function Clock() { 6 const [time, setTime] = useState(new Date()); 7 8 useEffect(() => { 9 const interval = setInterval(() => { 10 setTime(new Date()); 11 }, 1000); 12 13 // Cleanup function — runs before unmount or re-run 14 return () => clearInterval(interval); 15 }, []); // Empty dependency array = run once on mount 16 17 return <p className="text-xl">{time.toLocaleTimeString()}</p>; 18}
When to use: Subscriptions, timers, manual DOM manipulation, syncing with external systems.
Critical Next.js Lesson: Do not use
useEffectfor data fetching if you can fetch on the server instead. Server Components fetch data directly without hooks.
useContext — Global State Without Prop Drilling
1// contexts/ThemeContext.tsx 2"use client"; 3 4import { createContext, useContext, useState, ReactNode } from "react"; 5 6const ThemeContext = createContext<{ 7 theme: "light" | "dark"; 8 toggleTheme: () => void; 9} | null>(null); 10 11export function ThemeProvider({ children }: { children: ReactNode }) { 12 const [theme, setTheme] = useState<"light" | "dark">("light"); 13 14 const toggleTheme = () => { 15 setTheme((prev) => (prev === "light" ? "dark" : "light")); 16 }; 17 18 return ( 19 <ThemeContext.Provider value={{ theme, toggleTheme }}> 20 {children} 21 </ThemeContext.Provider> 22 ); 23} 24 25export function useTheme() { 26 const context = useContext(ThemeContext); 27 if (!context) throw new Error("useTheme must be inside ThemeProvider"); 28 return context; 29}
1// components/ui/ThemeToggle.tsx 2"use client"; 3 4import { useTheme } from "@/contexts/ThemeContext"; 5 6export function ThemeToggle() { 7 const { theme, toggleTheme } = useTheme(); 8 9 return ( 10 <button 11 onClick={toggleTheme} 12 className="px-4 py-2 border rounded-lg" 13 > 14 {theme === "light" ? "🌙 Dark Mode" : "☀️ Light Mode"} 15 </button> 16 ); 17}
When to use: Theme, user preferences, authentication state, application settings — any data many components need.
useReducer — Complex State Logic
When state logic becomes complex (multiple related values, next state depends on previous), useReducer is cleaner than multiple useState calls.
1"use client"; 2 3import { useReducer } from "react"; 4 5interface CartState { 6 items: { id: string; name: string; quantity: number }[]; 7 total: number; 8} 9 10type CartAction = 11 | { type: "ADD_ITEM"; payload: { id: string; name: string; price: number } } 12 | { type: "REMOVE_ITEM"; payload: { id: string } } 13 | { type: "CLEAR" }; 14 15function cartReducer(state: CartState, action: CartAction): CartState { 16 switch (action.type) { 17 case "ADD_ITEM": 18 return { 19 ...state, 20 items: [...state.items, { ...action.payload, quantity: 1 }], 21 }; 22 case "REMOVE_ITEM": 23 return { 24 ...state, 25 items: state.items.filter((item) => item.id !== action.payload.id), 26 }; 27 case "CLEAR": 28 return { items: [], total: 0 }; 29 default: 30 return state; 31 } 32} 33 34export function ShoppingCart() { 35 const [cart, dispatch] = useReducer(cartReducer, { items: [], total: 0 }); 36 37 return ( 38 <div> 39 <p>Items: {cart.items.length}</p> 40 <button 41 onClick={() => 42 dispatch({ 43 type: "ADD_ITEM", 44 payload: { id: "1", name: "Course", price: 99 }, 45 }) 46 } 47 > 48 Add Course 49 </button> 50 <button onClick={() => dispatch({ type: "CLEAR" })}>Clear Cart</button> 51 </div> 52 ); 53}
When to use: Forms with many fields, shopping carts, complex UI state machines.
useRef — DOM Access & Mutable Values
1"use client"; 2 3import { useRef } from "react"; 4 5export function FocusInput() { 6 const inputRef = useRef<HTMLInputElement>(null); 7 8 const handleFocus = () => { 9 inputRef.current?.focus(); 10 }; 11 12 return ( 13 <div className="flex gap-2"> 14 <input 15 ref={inputRef} 16 type="text" 17 placeholder="Click button to focus" 18 className="px-4 py-2 border rounded-lg" 19 /> 20 <button 21 onClick={handleFocus} 22 className="px-4 py-2 bg-blue-600 text-white rounded-lg" 23 > 24 Focus Input 25 </button> 26 </div> 27 ); 28}
When to use: Focus management, measuring DOM elements, storing previous values without triggering re-renders, integrating with non-React libraries.
useMemo — Expensive Computations
Cache expensive calculations so they only re-run when dependencies change.
1"use client"; 2 3import { useState, useMemo } from "react"; 4 5export function CourseFilter({ courses }: { courses: Course[] }) { 6 const [search, setSearch] = useState(""); 7 8 // Only recalculate when courses or search changes 9 const filteredCourses = useMemo(() => { 10 return courses.filter((course) => 11 course.title.toLowerCase().includes(search.toLowerCase()) 12 ); 13 }, [courses, search]); 14 15 return ( 16 <div> 17 <input 18 value={search} 19 onChange={(e) => setSearch(e.target.value)} 20 placeholder="Search courses..." 21 className="px-4 py-2 border rounded-lg w-full mb-4" 22 /> 23 <p className="text-sm text-gray-500 mb-2"> 24 {filteredCourses.length} courses found 25 </p> 26 {/* Render filteredCourses */} 27 </div> 28 ); 29}
When to use: Filtering large lists, sorting data, complex calculations, transforming API responses.
useCallback — Stable Function References
Prevent functions from being recreated on every render — critical when passing callbacks to optimized child components.
1"use client"; 2 3import { useState, useCallback } from "react"; 4import { CourseCard } from "./CourseCard"; 5 6export function CourseList({ courses }: { courses: Course[] }) { 7 const [likedCourses, setLikedCourses] = useState<Set<string>>(new Set()); 8 9 // Stable reference — CourseCard won't re-render unnecessarily 10 const handleLike = useCallback((courseId: string) => { 11 setLikedCourses((prev) => { 12 const next = new Set(prev); 13 if (next.has(courseId)) { 14 next.delete(courseId); 15 } else { 16 next.add(courseId); 17 } 18 return next; 19 }); 20 }, []); // No dependencies = never recreated 21 22 return ( 23 <div className="grid grid-cols-3 gap-6"> 24 {courses.map((course) => ( 25 <CourseCard 26 key={course.id} 27 course={course} 28 isLiked={likedCourses.has(course.id)} 29 onLike={handleLike} 30 /> 31 ))} 32 </div> 33 ); 34}
When to use: Passing callbacks to memoized child components, dependency arrays of useEffect, event handlers in large lists.
useId — Unique IDs for Accessibility
Generate stable, unique IDs for form labels and ARIA attributes.
1"use client"; 2 3import { useId } from "react"; 4 5export function EmailField() { 6 const id = useId(); // Generates a unique ID like ":r0:" 7 8 return ( 9 <div> 10 <label htmlFor={id} className="block text-sm font-medium mb-1"> 11 Email Address 12 </label> 13 <input 14 id={id} 15 type="email" 16 className="px-4 py-2 border rounded-lg w-full" 17 aria-describedby={`${id}-help`} 18 /> 19 <p id={`${id}-help`} className="text-xs text-gray-500 mt-1"> 20 We'll never share your email. 21 </p> 22 </div> 23 ); 24}
When to use: Form inputs, accessibility attributes, any scenario needing unique DOM IDs in reusable components.
useTransition — Non-Blocking Updates
Mark state updates as transitions so React keeps the UI responsive during expensive re-renders.
1"use client"; 2 3import { useState, useTransition } from "react"; 4 5export function CourseSearch({ courses }: { courses: Course[] }) { 6 const [query, setQuery] = useState(""); 7 const [results, setResults] = useState(courses); 8 const [isPending, startTransition] = useTransition(); 9 10 const handleSearch = (value: string) => { 11 setQuery(value); // Urgent update — input responds immediately 12 13 startTransition(() => { 14 // Non-urgent update — filtering can be interrupted 15 const filtered = courses.filter((c) => 16 c.title.toLowerCase().includes(value.toLowerCase()) 17 ); 18 setResults(filtered); 19 }); 20 }; 21 22 return ( 23 <div> 24 <input 25 value={query} 26 onChange={(e) => handleSearch(e.target.value)} 27 placeholder="Search courses..." 28 className="px-4 py-2 border rounded-lg w-full" 29 /> 30 {isPending && <p className="text-sm text-gray-400">Filtering...</p>} 31 <div className="grid grid-cols-3 gap-4 mt-4"> 32 {results.map((course) => ( 33 <CourseCard key={course.id} course={course} /> 34 ))} 35 </div> 36 </div> 37 ); 38}
When to use: Heavy list filtering, tab switching, any state update that causes noticeable UI lag.
useDeferredValue — Deferred Re-renders
Similar to useTransition but for values rather than state updates. Useful when a value from props causes expensive child renders.
1"use client"; 2 3import { useState, useDeferredValue } from "react"; 4import { CourseList } from "./CourseList"; 5 6export function SearchWithDeferred({ courses }: { courses: Course[] }) { 7 const [query, setQuery] = useState(""); 8 const deferredQuery = useDeferredValue(query); // Lags behind query 9 10 return ( 11 <div> 12 <input 13 value={query} 14 onChange={(e) => setQuery(e.target.value)} 15 placeholder="Search..." 16 className="px-4 py-2 border rounded-lg w-full" 17 /> 18 {/* CourseList receives the deferred value */} 19 <CourseList courses={courses} filter={deferredQuery} /> 20 </div> 21 ); 22}
When to use: Passing rapidly changing values to expensive child components, keeping input responsive while list catches up.
5. Next.js Example: When to Use Hooks vs. Server Fetching
❌ Bad: Fetching Data in useEffect
1"use client"; 2 3import { useState, useEffect } from "react"; 4 5export default function CoursesPage() { 6 const [courses, setCourses] = useState([]); 7 const [loading, setLoading] = useState(true); 8 9 useEffect(() => { 10 fetch("/api/courses") 11 .then((r) => r.json()) 12 .then((data) => { 13 setCourses(data); 14 setLoading(false); 15 }); 16 }, []); 17 18 if (loading) return <div>Loading...</div>; 19 20 return ( 21 <div> 22 {courses.map((course) => ( 23 <p key={course.id}>{course.title}</p> 24 ))} 25 </div> 26 ); 27}
Problems:
- ❌ SEO-unfriendly (empty HTML initially)
- ❌ Waterfall loading (page loads, then JS loads, then data fetches)
- ❌ More JavaScript sent to browser
- ❌ Loading state management complexity
✅ Good: Server-Side Fetching
1// app/courses/page.tsx — Server Component (no hooks needed!) 2import { CourseCard } from "@/components/courses/CourseCard"; 3 4export default async function CoursesPage() { 5 const courses = await fetch("https://api.example.com/courses", { 6 cache: "force-cache", 7 }).then((r) => r.json()); 8 9 return ( 10 <main className="max-w-6xl mx-auto px-4 py-12"> 11 <h1 className="text-3xl font-bold mb-8">All Courses</h1> 12 <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> 13 {courses.map((course) => ( 14 <CourseCard key={course.id} course={course} /> 15 ))} 16 </div> 17 </main> 18 ); 19}
Benefits:
- ✅ SEO-friendly (full HTML on first request)
- ✅ No loading spinners needed
- ✅ Zero client-side JavaScript for data fetching
- ✅ Faster Time to First Contentful Paint
✅ Good: Using Hooks for Interactivity
1// components/courses/CourseFilter.tsx 2"use client"; 3 4import { useState, useMemo, useTransition } from "react"; 5 6interface CourseFilterProps { 7 courses: Course[]; // Passed from Server Component 8} 9 10export function CourseFilter({ courses }: CourseFilterProps) { 11 const [category, setCategory] = useState("all"); 12 const [isPending, startTransition] = useTransition(); 13 14 const filtered = useMemo(() => { 15 if (category === "all") return courses; 16 return courses.filter((c) => c.category === category); 17 }, [courses, category]); 18 19 return ( 20 <div> 21 <select 22 value={category} 23 onChange={(e) => { 24 startTransition(() => { 25 setCategory(e.target.value); 26 }); 27 }} 28 className="px-4 py-2 border rounded-lg mb-4" 29 > 30 <option value="all">All Categories</option> 31 <option value="web">Web Development</option> 32 <option value="data">Data Science</option> 33 </select> 34 {isPending && <p>Updating...</p>} 35 <div className="grid grid-cols-3 gap-4"> 36 {filtered.map((course) => ( 37 <CourseCard key={course.id} course={course} /> 38 ))} 39 </div> 40 </div> 41 ); 42}
6. Real-World Example: Tech3Space Custom Hooks
Custom hooks extract reusable logic. They are the secret to clean, maintainable Next.js apps.
useLocalStorage Hook
1// hooks/useLocalStorage.ts 2"use client"; 3 4import { useState, useEffect } from "react"; 5 6export function useLocalStorage<T>(key: string, initialValue: T) { 7 const [storedValue, setStoredValue] = useState<T>(() => { 8 if (typeof window === "undefined") return initialValue; 9 try { 10 const item = window.localStorage.getItem(key); 11 return item ? (JSON.parse(item) as T) : initialValue; 12 } catch { 13 return initialValue; 14 } 15 }); 16 17 const setValue = (value: T | ((val: T) => T)) => { 18 try { 19 const valueToStore = value instanceof Function ? value(storedValue) : value; 20 setStoredValue(valueToStore); 21 if (typeof window !== "undefined") { 22 window.localStorage.setItem(key, JSON.stringify(valueToStore)); 23 } 24 } catch (error) { 25 console.error(`Error setting localStorage key "${key}":`, error); 26 } 27 }; 28 29 return [storedValue, setValue] as const; 30}
Usage:
1const [theme, setTheme] = useLocalStorage<"light" | "dark">("theme", "light");
useDebounce Hook
1// hooks/useDebounce.ts 2"use client"; 3 4import { useState, useEffect } from "react"; 5 6export function useDebounce<T>(value: T, delay: number): T { 7 const [debouncedValue, setDebouncedValue] = useState(value); 8 9 useEffect(() => { 10 const timer = setTimeout(() => setDebouncedValue(value), delay); 11 return () => clearTimeout(timer); 12 }, [value, delay]); 13 14 return debouncedValue; 15}
Usage:
1const [search, setSearch] = useState(""); 2const debouncedSearch = useDebounce(search, 300); 3 4// Only fetch when debouncedSearch changes 5useEffect(() => { 6 fetch(`/api/search?q=${debouncedSearch}`); 7}, [debouncedSearch]);
useAuth Hook
1// hooks/useAuth.ts 2"use client"; 3 4import { useState, useEffect } from "react"; 5 6interface User { 7 id: string; 8 name: string; 9 email: string; 10} 11 12export function useAuth() { 13 const [user, setUser] = useState<User | null>(null); 14 const [isLoading, setIsLoading] = useState(true); 15 16 useEffect(() => { 17 fetch("/api/auth/me") 18 .then((r) => r.json()) 19 .then((data) => { 20 setUser(data.user); 21 setIsLoading(false); 22 }) 23 .catch(() => setIsLoading(false)); 24 }, []); 25 26 return { user, isLoading, isAuthenticated: !!user }; 27}
7. Common Mistakes
❌ Mistake 1: Using useEffect for Every Data Fetch
1// WRONG — use server fetching instead 2"use client"; 3useEffect(() => { 4 fetch("/api/courses").then((r) => r.json()).then(setCourses); 5}, []);
1// CORRECT — Server Component 2export default async function Page() { 3 const courses = await fetch("/api/courses"); 4 return <CourseList courses={courses} />; 5}
❌ Mistake 2: Missing Cleanup in useEffect
1// WRONG — memory leak 2useEffect(() => { 3 const interval = setInterval(() => setTime(new Date()), 1000); 4 // ❌ No cleanup! 5}, []);
1// CORRECT — cleanup on unmount 2useEffect(() => { 3 const interval = setInterval(() => setTime(new Date()), 1000); 4 return () => clearInterval(interval); // ✅ Cleanup 5}, []);
❌ Mistake 3: Stale Closures in useEffect
1// WRONG — count is stale inside interval 2useEffect(() => { 3 const interval = setInterval(() => { 4 setCount(count + 1); // ❌ Always uses initial count value 5 }, 1000); 6}, []);
1// CORRECT — use functional update 2useEffect(() => { 3 const interval = setInterval(() => { 4 setCount((prev) => prev + 1); // ✅ Always uses latest value 5 }, 1000); 6 return () => clearInterval(interval); 7}, []);
❌ Mistake 4: Overusing useMemo and useCallback
1// WRONG — premature optimization 2const value = useMemo(() => a + b, [a, b]); // Simple addition is already fast
1// CORRECT — only memoize expensive operations 2const filtered = useMemo(() => { 3 return largeArray.filter(complexPredicate); // ✅ Worth memoizing 4}, [largeArray]);
❌ Mistake 5: Calling Hooks Conditionally
1// WRONG — hook inside condition 2if (isActive) { 3 const [count, setCount] = useState(0); 4}
1// CORRECT — hooks at top level 2const [count, setCount] = useState(0); 3if (!isActive) return null;
8. Best Practices
| Practice | Why |
|---|---|
| Fetch data in Server Components | Better SEO, less JS, simpler code |
| Use hooks only for interactivity | State, effects, refs, context |
| Always cleanup subscriptions | Prevents memory leaks |
| Use functional updates | Avoid stale closures |
| Keep custom hooks focused | One hook, one responsibility |
| Type your hooks | useState<Type>, typed returns |
Prefer useTransition for heavy updates | Keeps UI responsive |
Use useId for accessibility | Stable, unique IDs |
9. Architecture Explanation
Server vs. Client Hook Usage
Next.js Page (Server Component)
↓
Fetches data directly (no hooks)
↓
Renders static structure
↓
Passes data as props to Client Components
↓
Client Components use hooks for interactivity
├── useState → form inputs, toggles
├── useEffect → subscriptions, timers
├── useRef → DOM access
├── useContext → shared state
└── useMemo/useCallback → performance
When to Use Each Hook
| Hook | Use For | Next.js Context |
|---|---|---|
useState | Local component state | Forms, toggles, counters |
useEffect | Side effects, subscriptions | Timers, browser APIs, manual DOM |
useContext | Shared state across tree | Theme, auth, preferences |
useReducer | Complex state logic | Carts, forms, state machines |
useRef | DOM access, mutable values | Focus, measurements, previous values |
useMemo | Expensive computations | Filtering, sorting, transformations |
useCallback | Stable function references | Callbacks to memoized children |
useId | Unique DOM IDs | Accessibility, form labels |
useTransition | Non-blocking updates | Heavy list filtering |
useDeferredValue | Deferred re-renders | Search inputs with expensive lists |
10. Practice Task
Task: Build a useMediaQuery custom hook.
Requirements:
- Create
hooks/useMediaQuery.ts - Accept a media query string (e.g.,
"(min-width: 768px)") - Return
trueorfalsebased on whether the query matches - Update when the browser resizes
- Cleanup the listener on unmount
- Handle server-side rendering (return
falseinitially)
Usage:
1const isDesktop = useMediaQuery("(min-width: 1024px)"); 2const isMobile = useMediaQuery("(max-width: 768px)");
11. Mini Project: Tech3Space Course Enrollment Hook
Build a complete custom hook for course enrollment:
hooks/useEnrollment.ts
1"use client"; 2 3import { useState, useCallback } from "react"; 4 5interface EnrollmentState { 6 isEnrolled: boolean; 7 isLoading: boolean; 8 error: string | null; 9} 10 11export function useEnrollment(courseId: string) { 12 const [state, setState] = useState<EnrollmentState>({ 13 isEnrolled: false, 14 isLoading: false, 15 error: null, 16 }); 17 18 const enroll = useCallback(async () => { 19 setState((prev) => ({ ...prev, isLoading: true, error: null })); 20 21 try { 22 const response = await fetch(`/api/courses/${courseId}/enroll`, { 23 method: "POST", 24 }); 25 26 if (!response.ok) throw new Error("Enrollment failed"); 27 28 setState({ isEnrolled: true, isLoading: false, error: null }); 29 } catch (err) { 30 setState({ 31 isEnrolled: false, 32 isLoading: false, 33 error: err instanceof Error ? err.message : "Unknown error", 34 }); 35 } 36 }, [courseId]); 37 38 const unenroll = useCallback(async () => { 39 setState((prev) => ({ ...prev, isLoading: true, error: null })); 40 41 try { 42 const response = await fetch(`/api/courses/${courseId}/enroll`, { 43 method: "DELETE", 44 }); 45 46 if (!response.ok) throw new Error("Unenrollment failed"); 47 48 setState({ isEnrolled: false, isLoading: false, error: null }); 49 } catch (err) { 50 setState({ 51 isEnrolled: true, 52 isLoading: false, 53 error: err instanceof Error ? err.message : "Unknown error", 54 }); 55 } 56 }, [courseId]); 57 58 return { 59 ...state, 60 enroll, 61 unenroll, 62 }; 63}
Usage in Component
1// components/courses/EnrollButton.tsx 2"use client"; 3 4import { useEnrollment } from "@/hooks/useEnrollment"; 5import { Button } from "@/components/ui/Button"; 6 7interface EnrollButtonProps { 8 courseId: string; 9} 10 11export function EnrollButton({ courseId }: EnrollButtonProps) { 12 const { isEnrolled, isLoading, error, enroll, unenroll } = useEnrollment(courseId); 13 14 if (isLoading) { 15 return <Button disabled>Processing...</Button>; 16 } 17 18 return ( 19 <div> 20 {error && <p className="text-red-500 text-sm mb-2">{error}</p>} 21 <Button 22 onClick={isEnrolled ? unenroll : enroll} 23 variant={isEnrolled ? "secondary" : "primary"} 24 > 25 {isEnrolled ? "Unenroll" : "Enroll Now"} 26 </Button> 27 </div> 28 ); 29}
Summary
| Hook | Purpose | Key Lesson |
|---|---|---|
useState | Remember values between renders | Use functional updates for dependent state |
useEffect | Side effects & subscriptions | Always cleanup; prefer server fetching |
useContext | Share state without prop drilling | Create custom hook for consumption |
useReducer | Complex state logic | Great for forms and carts |
useRef | DOM access & mutable values | Does not trigger re-renders |
useMemo | Cache expensive calculations | Only for truly expensive operations |
useCallback | Stable function references | Use with React.memo children |
useId | Unique accessibility IDs | Never hardcode IDs |
useTransition | Non-blocking state updates | Keeps input responsive |
useDeferredValue | Lagging value updates | For expensive child re-renders |
What's Next?
In Module 11, we'll explore Server Components — the most important concept in modern Next.js. You'll learn why they exist, how they fetch data without hooks, and when to use them instead of Client Components.
Your mission: Build three custom hooks for your Tech3Space app:
useLocalStorage— Persist user preferencesuseDebounce— Delay search API callsuseEnrollment— Manage course enrollment state
Test each hook in a Client Component and verify they work correctly across renders and page refreshes!