Technology: React 18/19, Next.js, JavaScript/TypeScript
Table of Contents
- Course Overview
- Introduction to React Hooks
- Understanding React Hooks
- Built-in Hooks: useState, useEffect and useReducer
- Built-in Hooks: useRef, useContext, useMemo and useCallback
- Context and Custom Hooks for a Redux-like Experience
- React Hooks and Context for Modal Popup Forms
- Improving Performance with useMemo, memo and useCallback
- Improving UX with useDeferredValue and useTransition
- Managing Forms with useActionState and useFormStatus
- Course Summary
- Hook Reference Tables
Hook Categories — Overview
mindmap
root((React Hooks))
State
useState
useReducer
useActionState
Effect & Lifecycle
useEffect
useLayoutEffect
Context & Data
useContext
use
Ref & DOM
useRef
useImperativeHandle
Performance
useMemo
useCallback
memo
UI & Transitions
useTransition
useDeferredValue
useFormStatus
New in React 19
use
useActionState
useFormStatus
useOptimistic
1. Course Overview
Objective
This course teaches the React hooks built into the React library, from foundational hooks (useState, useEffect) through advanced hooks (useTransition, useActionState). By the end, you will be able to build enterprise-quality React applications that fully leverage React’s capabilities.
Demo Application
The application built throughout the course is a conference site that:
- Loads a list of speakers from a REST server (Next.js API routes)
- Allows dynamic filtering by name and day
- Manages favorites with optimistic updates (optimistic UI)
- Features an edit/add/delete speakers modal
- Integrates a theme switcher (light/dark)
- Includes a sign-up form (
useActionState)
Prerequisites
- Mastery of React function components
- Understanding of props and JSX
- Modern JavaScript: arrow functions, spread operators, async/await
2. Introduction to React Hooks
What is a React Hook?
A React hook is an ordinary JavaScript function that:
- Starts with the letters
use(required convention) - Can only be called from inside a function component
- Provides access to state and React features in function components
Why Hooks?
| Before Hooks | With Hooks |
|---|---|
| Class components for state | Function components are sufficient |
this.setState hard to understand | useState is intuitive |
| Fragmented lifecycle methods | useEffect is unified |
| Complex logic reuse | Simple custom hooks |
Hook Benefits
- Reusable logic: custom hooks allow sharing state logic
- Separation of concerns: decoupling between logic and rendering
- Composition: multiple hooks can be combined in a single component
Fundamental Example
import { useState, useEffect } from 'react';
function HelloComponent() {
const [helloMessage, setHelloMessage] = useState('Hello from the course');
useEffect(() => {
document.title = `Message: ${helloMessage}`;
}, [helloMessage]);
return (
<div>
<input
value={helloMessage}
onChange={(e) => setHelloMessage(e.target.value)}
/>
<p>{helloMessage}</p>
</div>
);
}
Typical React Application Structure
graph TD
App["App (root)"]
Layout["Layout"]
Header["Header"]
Speakers["Speakers"]
SpeakerMenu["SpeakerMenu"]
SpeakersList["SpeakersList"]
SpeakerDetail["SpeakerDetail"]
SpeakerImage["SpeakerImage"]
App --> Layout
Layout --> Header
Layout --> Speakers
Speakers --> SpeakerMenu
Speakers --> SpeakersList
SpeakersList --> SpeakerDetail
SpeakerDetail --> SpeakerImage
3. Understanding React Hooks
The Three Rules of Hooks (Rules of Hooks)
flowchart TD
R1["Rule 1\nHooks only inside\nReact function components"]
R2["Rule 2\nHooks cannot\nbe called conditionally"]
R3["Rule 3\nHooks only at\nthe top level of a component"]
R1 --> Valid1["✅ function MyComponent() { useState() }"]
R1 --> Invalid1["❌ class MyComponent extends React.Component { useState() }"]
R2 --> Valid2["✅ const [x] = useState(0)"]
R2 --> Invalid2["❌ if (condition) { const [x] = useState(0) }"]
R3 --> Valid3["✅ Top level call"]
R3 --> Invalid3["❌ Nested inside function/loop"]
ESLint: The
next/core-web-vitalsconfiguration in.eslintrc.jsonenables automatic detection of hook rule violations.
useState — Demystified
To understand useState, here is a simplified implementation:
// Naive implementation of useState (educational only)
const localStateValues = [];
let localStateValueIndex = 0;
function useMyState(initialValue) {
const indexLocal = localStateValueIndex;
if (localStateValues[indexLocal] === undefined) {
localStateValues[indexLocal] = initialValue;
}
function setValue(newValue) {
localStateValues[indexLocal] = newValue;
// Triggers a component re-render
forceRerender();
}
localStateValueIndex++;
return [localStateValues[indexLocal], setValue];
}
Key insights:
- React maintains a state array indexed by hook call order
- That’s why hooks cannot be called conditionally — the index would shift
- Each
useStatecall occupies a different index in this array
useEffect — Execution Flow
sequenceDiagram
participant Browser
participant React
participant Component
participant Effect
Browser->>React: Render requested
React->>Component: Execute function component
Component->>React: Returns JSX
React->>Browser: DOM update
React->>Effect: Execute useEffect function
Effect->>Effect: Side effects (fetch, subscriptions...)
Note over Effect: If cleanup function returned
Effect->>Effect: Cleanup on next render or unmount
useEffect Parameters:
// ❌ Runs on every render
useEffect(() => { /* ... */ });
// ✅ Runs once (mount)
useEffect(() => { /* ... */ }, []);
// ✅ Runs when 'value' changes
useEffect(() => { /* ... */ }, [value]);
// ✅ With cleanup
useEffect(() => {
const subscription = subscribe(event);
return () => {
subscription.unsubscribe(); // cleanup
};
}, [event]);
4. Built-in Hooks: useState, useEffect and useReducer
Categories of React Built-in Hooks
graph LR
subgraph "Basic Hooks"
useState
useEffect
useContext
end
subgraph "Additional Hooks"
useReducer
useCallback
useMemo
useRef
useImperativeHandle
useLayoutEffect
useDebugValue
end
subgraph "React 18+ Hooks"
useDeferredValue
useTransition
useId
end
subgraph "React 19 Hooks"
useActionState["useActionState\n(formerly useFormState)"]
useFormStatus
useOptimistic
use["use (new)"]
end
useState — Full API
import { useState } from 'react';
function SpeakerList() {
// Basic syntax
const [speakers, setSpeakers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
// Update based on previous state (recommended)
const toggleFavorite = (id) => {
setSpeakers(prevSpeakers =>
prevSpeakers.map(speaker =>
speaker.id === id
? { ...speaker, isFavorite: !speaker.isFavorite }
: speaker
)
);
};
// ⚠️ NEVER mutate state directly
// ❌ speakers.push(newSpeaker)
// ✅ setSpeakers([...speakers, newSpeaker])
}
useEffect — Loading Data from REST
import { useState, useEffect } from 'react';
import axios from 'axios';
function SpeakerList() {
const [speakers, setSpeakers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
axios.get('/api/speakers')
.then(({ data }) => {
setSpeakers(data);
setIsLoading(false);
});
}, []); // [] = runs once on mount
const toggleFavoriteSpeaker = async (speakerRec) => {
// Optimistic local state update
const updatedSpeakers = speakers.map(speaker =>
speaker.id === speakerRec.id
? { ...speaker, isFavorite: !speaker.isFavorite }
: { ...speaker }
);
setSpeakers(updatedSpeakers);
// Sync with server
await axios.put(`/api/speakers/${speakerRec.id}`, speakerRec);
};
if (isLoading) return <div>Loading...</div>;
return (
<div>
{speakers.map(speaker => (
<SpeakerCard
key={speaker.id}
speaker={speaker}
onToggleFavorite={toggleFavoriteSpeaker}
/>
))}
</div>
);
}
useReducer — Concept
useReducer is a more flexible alternative to useState. It is particularly useful when:
- Multiple states are related to each other
- The update logic is complex
- You want an architecture similar to Redux
flowchart LR
Component["Component"]
Dispatch["dispatch(action)"]
Reducer["reducer(state, action)"]
NewState["New state"]
Rerender["Re-render"]
Component -->|"onClick, etc."| Dispatch
Dispatch -->|"action = { type, payload }"| Reducer
Reducer --> NewState
NewState --> Rerender
Rerender --> Component
useReducer — useState vs useReducer Comparison
// With useState
function CounterWithState() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
// With useReducer — same result, more control
function CounterWithReducer() {
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT': return { count: state.count + 1 };
case 'DECREMENT': return { count: state.count - 1 };
default: return state;
}
};
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
<span>{state.count}</span>
<button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
</>
);
}
useReducer — SpeakerList Refactoring
const initialState = {
speakers: [],
isLoading: true,
};
function reducer(state, action) {
switch (action.type) {
case 'SPEAKERS_LOADED':
return { ...state, isLoading: false, speakers: action.speakers };
case 'TOGGLE_FAVORITE':
return {
...state,
speakers: state.speakers.map(s =>
s.id === action.speaker.id
? { ...s, isFavorite: !s.isFavorite }
: s
),
};
default:
return state;
}
}
function SpeakerList() {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
axios.get('/api/speakers').then(({ data }) => {
dispatch({ type: 'SPEAKERS_LOADED', speakers: data });
});
}, []);
const toggleFavorite = (speaker) => {
dispatch({ type: 'TOGGLE_FAVORITE', speaker });
axios.put(`/api/speakers/${speaker.id}`, speaker);
};
// ...
}
useReducer advantages:
- A single state update triggers a single re-render (even if multiple properties change)
- The logic is testable independently of the component
- Compatible with external state managers like Redux
5. Built-in Hooks: useRef, useContext, useMemo and useCallback
useRef — Two Use Cases
graph TD
useRef["useRef(initialValue)"]
useRef --> DOM["Direct DOM access\n(escape hatch)"]
useRef --> Persist["Persistent variable\nWithout re-render"]
DOM --> Focus["setFocus on an input"]
DOM --> Scroll["Scroll to an element"]
DOM --> Colorize["Detect if element is visible (IntersectionObserver)"]
Persist --> Counter["Count renders"]
Persist --> Timer["Store a timer ID"]
Persist --> PrevValue["Keep the previous value"]
import { useRef, useState, useEffect } from 'react';
function SpeakerImageToggleOnScroll({ imageUrl, altTag }) {
const imageRef = useRef(null);
const [inView, setInView] = useState(false);
const isInView = () => {
if (!imageRef.current) return false;
const rect = imageRef.current.getBoundingClientRect();
return rect.top >= 0 && rect.bottom <= window.innerHeight;
};
useEffect(() => {
setInView(isInView());
const handleScroll = () => setInView(isInView());
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const grayScale = inView ? 'grayscale(0%)' : 'grayscale(100%)';
return (
<img
ref={imageRef}
src={imageUrl}
alt={altTag}
style={{ filter: grayScale, transition: 'filter 0.3s' }}
/>
);
}
useContext — Sharing Data in the Component Tree
Problem without Context: “prop drilling” — passing props through many levels of components.
graph TD
App["App\ntheme = 'dark'"]
Layout["Layout\ntheme={theme}"]
Header["Header\ntheme={theme}"]
ThemeToggle["ThemeToggle\ntheme={theme}"]
App -->|"prop drilling ❌"| Layout
Layout -->|"prop drilling ❌"| Header
Header -->|"prop drilling ❌"| ThemeToggle
Solution with Context:
graph TD
ThemeProvider["ThemeProvider\n(Context.Provider)"]
Layout["Layout"]
Header["Header"]
ThemeToggle["ThemeToggle\nuseContext → theme ✅"]
ThemeProvider --> Layout
Layout --> Header
Header --> ThemeToggle
useContext — Theme Switcher Implementation
// themeContext.js
import { createContext, useState } from 'react';
export const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const [darkTheme, setDarkTheme] = useState(false);
const toggleTheme = () => setDarkTheme(prev => !prev);
return (
<ThemeContext.Provider value={{ darkTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// In any child component
import { useContext } from 'react';
import { ThemeContext } from './themeContext';
function ThemeToggleButton() {
const { darkTheme, toggleTheme } = useContext(ThemeContext);
return (
<button
onClick={toggleTheme}
className={darkTheme ? 'btn-dark' : 'btn-light'}
>
{darkTheme ? '☀️ Light' : '🌙 Dark'}
</button>
);
}
use — The New React 19 Hook
use is a special hook introduced in React 19 that can be called conditionally (unlike useContext):
import { use } from 'react';
import { ThemeContext } from './themeContext';
function ThemeToggleButton() {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => setIsMounted(true), []);
// ✅ use can be called conditionally
if (!isMounted) return null;
const { darkTheme, toggleTheme } = use(ThemeContext);
return <button onClick={toggleTheme}>Toggle</button>;
}
Differences between use and useContext:
| Feature | useContext | use |
|---|---|---|
| Conditional call | ❌ Forbidden | ✅ Allowed |
| Suspense support | ❌ | ✅ |
| Promises support | ❌ | ✅ |
| React version | 16.3+ | 19+ |
useMemo and useCallback — Performance Optimization
import { useMemo, useCallback, memo } from 'react';
// useMemo: memoizes the result of an expensive computation
const filteredSpeakers = useMemo(() => {
return speakers.filter(s =>
s.firstName.includes(search) || s.lastName.includes(search)
);
}, [speakers, search]); // Recomputed only if speakers or search changes
// useCallback: memoizes a function (stable reference)
const toggleFavorite = useCallback((speakerId) => {
setSpeakers(prev =>
prev.map(s => s.id === speakerId ? { ...s, isFavorite: !s.isFavorite } : s)
);
}, []); // Stable reference — never changes
// memo: prevents unnecessary re-renders of a component
const SpeakerLine = memo(function SpeakerLine({ speaker, onToggle }) {
return (
<div>
<span>{speaker.firstName} {speaker.lastName}</span>
<button onClick={() => onToggle(speaker.id)}>♥</button>
</div>
);
});
6. Context and Custom Hooks for a Redux-like Experience
Recommended Architecture
graph LR
subgraph "Files"
Hook["hooks/useSpeakersData.js\nCustom Hook"]
Context["contexts/SpeakersDataContext.js\nContext Provider"]
Component["components/SpeakersList.jsx\nConsumer"]
end
Hook -->|"manages state and REST calls"| Context
Context -->|"exposes via useContext"| Component
Principle: separate state logic (custom hook) from data sharing (context) from display (component).
Custom Hook — useSpeakersData
// hooks/useSpeakersData.js
import { useState, useEffect } from 'react';
import axios from 'axios';
export function useSpeakersData() {
const [speakers, setSpeakers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
axios.get('/api/speakers')
.then(({ data }) => {
setSpeakers(data);
setIsLoading(false);
})
.catch(err => setError(err.message));
}, []);
const toggleFavorite = async (speakerRec) => {
const updatedSpeakers = speakers.map(s =>
s.id === speakerRec.id ? { ...s, isFavorite: !s.isFavorite } : s
);
setSpeakers(updatedSpeakers);
await axios.put(`/api/speakers/${speakerRec.id}`, speakerRec);
};
const addSpeaker = async (newSpeaker) => {
const { data } = await axios.post('/api/speakers', newSpeaker);
setSpeakers([...speakers, data]);
};
const deleteSpeaker = async (id) => {
await axios.delete(`/api/speakers/${id}`);
setSpeakers(speakers.filter(s => s.id !== id));
};
return { speakers, isLoading, error, toggleFavorite, addSpeaker, deleteSpeaker };
}
Context Provider — SpeakersDataContext
// contexts/SpeakersDataContext.js
import { createContext, useContext } from 'react';
import { useSpeakersData } from '../hooks/useSpeakersData';
export const SpeakersDataContext = createContext(null);
export function SpeakersDataProvider({ children }) {
const speakersData = useSpeakersData();
return (
<SpeakersDataContext.Provider value={speakersData}>
{children}
</SpeakersDataContext.Provider>
);
}
// Utility hook to consume the context
export function useSpeakersContext() {
const context = useContext(SpeakersDataContext);
if (!context) throw new Error('useSpeakersContext must be used within SpeakersDataProvider');
return context;
}
Custom Hook for Theme
// hooks/useTheme.js
import { useState } from 'react';
export function useTheme() {
const [darkTheme, setDarkTheme] = useState(false);
const toggleTheme = () => setDarkTheme(prev => !prev);
return { darkTheme, toggleTheme };
}
// themeContext.js — uses the custom hook
import { createContext } from 'react';
import { useTheme } from '../hooks/useTheme';
export const ThemeContext = createContext(null);
export function ThemeProvider({ children }) {
const value = useTheme(); // Delegates logic to the custom hook
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
Optimization: Context Provider Placement
Rule: place Context Providers as low as possible in the component tree to minimize unnecessary re-renders.
graph TD
App["App"]
Layout["Layout ← ThemeProvider HERE ✅"]
Header["Header"]
SpeakerSection["Speakers\n← SpeakersDataProvider HERE ✅"]
SpeakersList["SpeakersList"]
SpeakerDetail["SpeakerDetail"]
App --> Layout
Layout --> Header
Layout --> SpeakerSection
SpeakerSection --> SpeakersList
SpeakersList --> SpeakerDetail
7. React Hooks and Context for Modal Popup Forms
React Modal Architecture
sequenceDiagram
participant User
participant SpeakerDetail
participant SpeakerModalContext
participant SpeakerModal
User->>SpeakerDetail: Clicks "Edit"
SpeakerDetail->>SpeakerModalContext: setModalShow(true) + setSpeakerData(rec)
SpeakerModalContext-->>SpeakerModal: isVisible = true, data = rec
SpeakerModal->>User: Shows the dialog
User->>SpeakerModal: Edits and saves
SpeakerModal->>SpeakersDataContext: updateSpeaker(newRec)
SpeakerModal->>SpeakerModalContext: setModalShow(false)
SpeakerModalContext-->>SpeakerModal: isVisible = false
SpeakerModal->>User: Dialog hidden
SpeakerModalContext
// contexts/SpeakerModalContext.js
import { createContext } from 'react';
import { useSpeakerModal } from '../hooks/useSpeakerModal';
export const SpeakerModalContext = createContext(null);
export function SpeakerModalProvider({ children }) {
const modalState = useSpeakerModal();
return (
<SpeakerModalContext.Provider value={modalState}>
{children}
</SpeakerModalContext.Provider>
);
}
// hooks/useSpeakerModal.js
import { useState } from 'react';
export function useSpeakerModal() {
const [modalShow, setModalShow] = useState(false);
const [speakerId, setSpeakerId] = useState(null);
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [bio, setBio] = useState('');
return {
modalShow, setModalShow,
speakerId, setSpeakerId,
firstName, setFirstName,
lastName, setLastName,
bio, setBio,
};
}
SpeakerDetail — Edit Button
function EditSpeakerDialog({ speakerRec }) {
const {
setModalShow, setSpeakerId,
setFirstName, setLastName
} = useContext(SpeakerModalContext);
const handleEdit = () => {
setSpeakerId(speakerRec.id);
setFirstName(speakerRec.firstName);
setLastName(speakerRec.lastName);
setModalShow(true); // Triggers the modal display
};
return <button onClick={handleEdit}>Edit</button>;
}
SpeakerModal — Component
function SpeakerModal() {
const { modalShow } = useContext(SpeakerModalContext);
const cssVisibility = modalShow
? 'modal fade show d-block'
: 'modal fade';
return (
<div className={cssVisibility} role="dialog">
<div className="modal-dialog">
<div className="modal-content">
<SpeakerModalHeader />
<SpeakerModalBody />
<SpeakerModalFooter />
</div>
</div>
{modalShow && <div className="modal-backdrop fade show" />}
</div>
);
}
8. Improving Performance with useMemo, memo and useCallback
When to Optimize?
Golden rule: only optimize when there is a measurable performance problem. The React engine is generally performant enough.
The React Compiler (React 19) automatically inserts useMemo and useCallback into your code at compile time, making these optimizations often unnecessary to do manually.
Re-render Flow
flowchart TD
StateChange["State change\n(e.g.: toggleFavorite)"]
ParentRender["Parent re-render\n(SpeakersList)"]
ChildRender["All children re-render\n(SpeakerLine × N)"]
Problem["❌ Performance problem\nif N is large"]
StateChange --> ParentRender
ParentRender --> ChildRender
ChildRender --> Problem
memoHook["memo() on SpeakerLine"]
useCallback["useCallback() on toggleFavorite"]
Optimized["✅ Only the affected SpeakerLine re-renders"]
Problem -.->|"Solution"| memoHook
Problem -.->|"Solution"| useCallback
memoHook --> Optimized
useCallback --> Optimized
useMemo — Filter Hook Refactoring
// hooks/useSpeakerSortAndFilter.js (pure version)
export function useSpeakerSortAndFilter(speakerList, searchText, saturday, sunday) {
return speakerList
.filter(speaker => {
const matchesSearch = !searchText ||
`${speaker.firstName} ${speaker.lastName}`
.toLowerCase()
.includes(searchText.toLowerCase());
const matchesSaturday = !saturday || speaker.speakingSaturday;
const matchesSunday = !sunday || speaker.speakingSunday;
return matchesSearch && matchesSaturday && matchesSunday;
})
.sort((a, b) => a.firstName.localeCompare(b.firstName));
}
// In SpeakersList.jsx
function SpeakersList() {
const { speakers } = useContext(SpeakersDataContext);
const { searchText, speakingSaturday, speakingSunday } = useContext(SpeakerMenuContext);
// ✅ Recomputed only when the filter or speakers change
const filteredSpeakers = useMemo(
() => useSpeakerSortAndFilter(speakers, searchText, speakingSaturday, speakingSunday),
[speakers, searchText, speakingSaturday, speakingSunday]
);
const toggleFavorite = useCallback((speakerId) => {
// ...
}, [speakers]);
return filteredSpeakers.map(speaker => (
<SpeakerLine
key={speaker.id}
speaker={speaker}
onToggle={toggleFavorite}
/>
));
}
// ✅ memo prevents re-render if props haven't changed
const SpeakerLine = memo(function SpeakerLine({ speaker, onToggle }) {
return (
<div>
<span>{speaker.firstName} {speaker.lastName}</span>
<button onClick={() => onToggle(speaker.id)}>
{speaker.isFavorite ? '❤️' : '🤍'}
</button>
</div>
);
});
Comparative Table of Performance Tools
| Tool | Type | Usage | Re-render Avoided? |
|---|---|---|---|
useMemo | Hook | Memoizes result of an expensive computation | Yes (computation) |
useCallback | Hook | Memoizes a function reference | Yes (stable reference) |
memo | HOC | Prevents re-render if props unchanged | Yes (component) |
| React Compiler | Tool | Automatically inserts the 3 above | Automatic |
9. Improving UX with useDeferredValue and useTransition
Problem: UI Blocked During Long Computation
sequenceDiagram
participant User
participant Input
participant React
participant SlowList
User->>Input: Types a letter
Input->>React: onChange → setSearch
React->>SlowList: Re-render (heavy computation)
Note over React,SlowList: ⚠️ Input is blocked during computation
SlowList-->>React: Render complete
React-->>Input: UI updated (feels slow)
useDeferredValue — Low Priority on a Value
useDeferredValue is ideal when you don’t have access to the code that updates the state (third-party library, etc.):
import { useState, useDeferredValue } from 'react';
function SearchSpeakers({ allSpeakers }) {
const [searchName, setSearchName] = useState('');
// deferredSearch follows searchName with low priority
const deferredSearch = useDeferredValue(searchName);
const isPending = deferredSearch !== searchName;
return (
<>
<input
value={searchName}
onChange={(e) => setSearchName(e.target.value)}
placeholder="Search speakers..."
/>
{/* Loading indicator while deferredSearch catches up */}
{isPending && <span>Updating...</span>}
{/* Receives the deferred value — doesn't block the input */}
<SpeakerHighlightList
speakers={allSpeakers}
highlight={deferredSearch}
/>
</>
);
}
useTransition — Full Control Over Priorities
useTransition is ideal when you control the code that updates state:
import { useState, useTransition } from 'react';
function SearchSpeakers({ allSpeakers }) {
const [searchName, setSearchName] = useState('');
const [highlightChars, setHighlightChars] = useState('');
const [isPending, startTransition] = useTransition();
const handleSearch = (e) => {
const value = e.target.value;
// HIGH priority — updated immediately
setSearchName(value);
// LOW priority — updated last
startTransition(() => {
setHighlightChars(value);
});
};
return (
<>
<div style={{ position: 'relative' }}>
<input
value={searchName}
onChange={handleSearch}
placeholder="Search speakers..."
/>
{isPending && (
<span className="spinner-border spinner-border-sm" />
)}
</div>
<SpeakerHighlightList
speakers={allSpeakers}
highlight={highlightChars} // Receives the low-priority value
/>
</>
);
}
useDeferredValue vs useTransition Comparison
| Criterion | useDeferredValue | useTransition |
|---|---|---|
| Source code control | Not required | Required |
isPending available | Via comparison value !== deferred | ✅ Directly returned |
| Loading indicator | Manual implementation | ✅ Native with isPending |
| Typical use case | Value that “follows” another | Controlled state update |
| React version | 18+ | 18+ |
10. Managing Forms with useActionState and useFormStatus
Evolution of Form Management
timeline
title React Forms Evolution
Classic HTML : form action="/submit" method="POST"
: Redirect after submit ❌
React useState : Full control with useState
: Lots of boilerplate ❌
useActionState : Delegates management to hook
: Less code ✅
: Server Functions compatible ✅
React 19 full : useActionState + useFormStatus
: Zod client + server validation ✅
: React Server Functions ✅
Classic React Form with useState (Problems)
// ❌ Lots of boilerplate
function SignupFormOld() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const [isPending, setIsPending] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsPending(true);
try {
const res = await fetch('/api/signup', {
method: 'POST',
body: JSON.stringify({ firstName, lastName, email }),
headers: { 'Content-Type': 'application/json' },
});
const data = await res.json();
setIsSuccess(res.ok);
setMessage(data.message);
} finally {
setIsPending(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input value={firstName} onChange={e => setFirstName(e.target.value)} />
<input value={lastName} onChange={e => setLastName(e.target.value)} />
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
<button type="submit" disabled={isPending}>
{isPending ? 'Submitting...' : 'Submit'}
</button>
{message && <p className={isSuccess ? 'text-success' : 'text-danger'}>{message}</p>}
</form>
);
}
useActionState — Drastic Simplification
useActionState works like useReducer but for forms. It automatically handles isPending state and asynchronous execution:
flowchart LR
Form["<form onSubmit>"]
FormData["FormData\n(form data)"]
ActionFn["Action Function\n(async)"]
NewState["New state returned"]
UIUpdate["UI updated"]
Form -->|"Submit"| FormData
FormData --> ActionFn
ActionFn --> NewState
NewState --> UIUpdate
Action Function (Client Side)
// actions/restSignupAction.js
export async function restSignupAction(prevState, formData) {
const payload = {
firstName: formData.get('firstName'),
lastName: formData.get('lastName'),
email: formData.get('email'),
};
try {
const res = await fetch('/api/signup', {
method: 'POST',
body: JSON.stringify(payload),
headers: { 'Content-Type': 'application/json' },
});
const data = await res.json();
return {
message: data.message,
isSuccess: res.ok,
firstName: res.ok ? '' : payload.firstName,
lastName: res.ok ? '' : payload.lastName,
email: res.ok ? '' : payload.email,
};
} catch (error) {
return { message: error.message, isSuccess: false, ...payload };
}
}
SignupForm with useActionState
import { useActionState, startTransition } from 'react';
import { restSignupAction } from './actions/restSignupAction';
import SubmitButton from './SubmitButton';
const initialSignupState = {
message: '',
isSuccess: false,
firstName: '',
lastName: '',
email: '',
};
function SignupForm() {
const [state, formAction, isPending] = useActionState(
restSignupAction,
initialSignupState
);
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
startTransition(() => formAction(formData));
};
return (
// key forces form reset after success
<form
key={`${state.message}-${state.isSuccess}`}
onSubmit={handleSubmit}
>
<input
name="firstName"
required
defaultValue={state.firstName}
placeholder="First Name"
/>
<input
name="lastName"
required
minLength={2}
defaultValue={state.lastName}
placeholder="Last Name"
/>
<input
name="email"
type="email"
required
defaultValue={state.email}
placeholder="Email"
/>
<SubmitButton />
{state.message && (
<p className={state.isSuccess ? 'text-success' : 'text-danger'}>
{state.message}
</p>
)}
</form>
);
}
useFormStatus — Smart SubmitButton
useFormStatus must be called from a child component of the <form>:
// SubmitButton.js
import { useFormStatus } from 'react-dom'; // Note: react-dom, not react
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? (
<>
<span className="spinner-border spinner-border-sm me-2" />
Submitting...
</>
) : (
'Submit'
)}
</button>
);
}
React Server Functions — To the Server
// actions/serverSignupAction.js
'use server'; // Next.js directive — code runs server-side
export async function serverSignupAction(prevState, formData) {
const payload = {
firstName: formData.get('firstName'),
lastName: formData.get('lastName'),
email: formData.get('email'),
};
// Server-side validation with database access
if (!payload.firstName || !payload.lastName) {
return { message: 'First and last name required', isSuccess: false, ...payload };
}
// Direct database access possible here
// const existing = await db.users.findByEmail(payload.email);
return {
message: `Welcome, ${payload.firstName}!`,
isSuccess: true,
firstName: '', lastName: '', email: ''
};
}
Validation with Zod — Client and Server
// schemas/signupSchema.js
import { z } from 'zod';
export const signupSchema = z.object({
firstName: z.string().min(1, 'First name required').max(100),
lastName: z.string().min(2, 'Last name must be at least 2 characters').max(100),
email: z.string().email('Invalid email format'),
});
export const initialSignupState = {
message: '',
isSuccess: false,
firstName: '',
lastName: '',
email: '',
};
// Client-side validation in handleSubmit
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const payload = Object.fromEntries(formData);
const result = signupSchema.safeParse(payload);
if (!result.success) {
const errors = result.error.flatten().fieldErrors;
// Display errors in UI
setValidationErrors(errors);
return;
}
startTransition(() => formAction(formData));
};
// Server-side validation in serverSignupAction
export async function serverSignupAction(prevState, formData) {
const payload = Object.fromEntries(formData);
const result = signupSchema.safeParse(payload);
if (!result.success) {
return { message: result.error.flatten().formErrors[0], isSuccess: false, ...payload };
}
// result.data is guaranteed valid by Zod
const validData = result.data;
// ...
}
11. Course Summary
Complete Application Flow
flowchart TD
App["App"]
ThemeProvider["ThemeProvider\n(ThemeContext)"]
Layout["Layout"]
Header["Header"]
Speakers["Speakers"]
SpeakersDataProvider["SpeakersDataProvider\n(SpeakersDataContext)"]
SpeakerMenuProvider["SpeakerMenuProvider\n(SpeakerMenuContext)"]
SpeakerMenu["SpeakerMenu\nuseContext(SpeakerMenuContext)"]
SpeakersList["SpeakersList\nuseContext(SpeakersDataContext)\nuseMemo(filter)"]
SpeakerDetail["SpeakerDetail"]
SpeakerModalProvider["SpeakerModalProvider\n(SpeakerModalContext)"]
SpeakerModal["SpeakerModal\nuseContext(SpeakerModalContext)"]
SpeakerImage["SpeakerImageToggle\nuseRef + useEffect"]
App --> ThemeProvider
ThemeProvider --> Layout
Layout --> Header
Layout --> Speakers
Speakers --> SpeakersDataProvider
SpeakersDataProvider --> SpeakerMenuProvider
SpeakerMenuProvider --> SpeakerMenu
SpeakerMenuProvider --> SpeakersList
SpeakersList --> SpeakerDetail
SpeakerDetail --> SpeakerModalProvider
SpeakerModalProvider --> SpeakerModal
SpeakerDetail --> SpeakerImage
Hook Rules — Visual Summary
flowchart LR
Rule1["✅ Only inside\nfunction components"]
Rule2["✅ At top level\nnot inside conditions"]
Rule3["✅ Name starts\nwith 'use'"]
Violation1["❌ Class components"]
Violation2["❌ if/else/loops"]
Violation3["❌ Ordinary functions"]
Rule1 -.->|"Violation"| Violation1
Rule2 -.->|"Violation"| Violation2
Rule3 -.->|"Violation"| Violation3
Complexity Progression
| Module | Hook(s) | Key Concept |
|---|---|---|
| 3 | useState, useEffect | Fundamentals, rules of hooks |
| 4 | useState, useEffect, useReducer | State management patterns |
| 5 | useRef, useContext, use, useMemo, useCallback | DOM access, data sharing |
| 6 | useContext + custom hooks | Redux-like architecture |
| 7 | useContext | Modal dialogs |
| 8 | useMemo, useCallback, memo | Performance optimization |
| 9 | useDeferredValue, useTransition | UI responsiveness |
| 10 | useActionState, useFormStatus | Forms + Server Functions |
12. Hook Reference Tables
All React Hooks — Complete Reference
| Hook | Import | Returns | Primary Usage |
|---|---|---|---|
useState | react | [state, setter] | Local component state |
useEffect | react | void | Side effects, lifecycle |
useContext | react | contextValue | Read a Context |
useReducer | react | [state, dispatch] | Complex / multiple state |
useRef | react | { current: T } | DOM reference or persistent value |
useMemo | react | memoizedValue | Memoize an expensive computation |
useCallback | react | memoizedFn | Stable function reference |
useLayoutEffect | react | void | Side effect before DOM paint |
useImperativeHandle | react | void | Imperative API with forwardRef |
useDebugValue | react | void | Label in React DevTools |
useId | react | string | Unique ID for accessibility |
useTransition | react | [isPending, startTransition] | Low priority on state |
useDeferredValue | react | deferredValue | Value with low priority |
use | react | contextValue | promiseResult | Conditional context + Suspense |
useActionState | react | [state, formAction, isPending] | Forms + server functions |
useFormStatus | react-dom | { pending, data, method, action } | Last submit status |
useOptimistic | react | [optimisticState, addOptimistic] | Optimistic UI during async |
TypeScript Signatures for Key Hooks
// useState
const [state, setState] = useState<T>(initialValue: T);
setState(newValue: T);
setState((prev: T) => newValue: T);
// useEffect
useEffect(effect: () => void | (() => void), deps?: DependencyList);
// useReducer
const [state, dispatch] = useReducer<R extends Reducer<any, any>>(
reducer: R,
initialState: ReducerState<R>,
init?: (arg: ReducerState<R>) => ReducerState<R>
);
// useContext
const value = useContext<T>(context: Context<T>);
// useRef
const ref = useRef<T>(initialValue: T | null); // → MutableRefObject<T>
const ref = useRef<T>(null); // → RefObject<T> for DOM elements
// useMemo
const memoized = useMemo<T>(() => T, deps: DependencyList);
// useCallback
const memoizedFn = useCallback<T extends Function>(fn: T, deps: DependencyList): T;
// useTransition
const [isPending, startTransition] = useTransition();
// useActionState (React 19)
const [state, formAction, isPending] = useActionState<S, P>(
action: (state: Awaited<S>, payload: P) => S | Promise<S>,
initialState: Awaited<S>,
permalink?: string
);
When to Use Each Hook?
flowchart TD
Q1{"Need to\nmaintain local\nstate?"}
Q1 -->|"Simple"| useState
Q1 -->|"Complex / multiple"| useReducer
Q1 -->|"No"| Q2
Q2{"Side effect\n(fetch, DOM, timer)?"}
Q2 -->|"Yes"| useEffect
Q2 -->|"No"| Q3
Q3{"Share data\nbetween components?"}
Q3 -->|"Yes"| useContext
Q3 -->|"No"| Q4
Q4{"Performance\noptimization?"}
Q4 -->|"Expensive computation"| useMemo
Q4 -->|"Function reference"| useCallback
Q4 -->|"Child component"| memo
Q5{"UI unresponsive\nduring computation?"}
Q5 -->|"Controls state"| useTransition
Q5 -->|"No control"| useDeferredValue
Q6{"Form with\nserver function?"}
Q6 -->|"Yes"| useActionState
Q6 -->|"Submit status"| useFormStatus
Common Pitfalls
| Pitfall | Bad Practice | Good Practice |
|---|---|---|
| Direct state mutation | state.items.push(x) | setState([...state.items, x]) |
| Conditional hook | if (cond) { useState() } | Always at top level |
| Missing dependencies | useEffect(() => f(x), []) | useEffect(() => f(x), [x]) |
| Object in deps | useEffect(() => {}, [{}]) | Memoize with useMemo |
| Forgetting cleanup | addEventListener without removeEventListener | Always return cleanup fn |
| useContext too high | Context at root → all re-render | Place as low as possible |
useFormStatus outside form | Called in parent of <form> | Called in child of <form> |
Search Terms
hooks · react · typescript · frontend · development · hook · usestate · context · usememo · usereducer · application · built-in · custom · flow · performance · useactionstate · usecallback · usecontext · usedeferredvalue · useeffect · usetransition · architecture · categories · client