Intermediate

Using Hooks in React 19

Built-in hooks and custom hooks, performance with memoization and forms with useActionState.

Technology: React 18/19, Next.js, JavaScript/TypeScript


Table of Contents

  1. Course Overview
  2. Introduction to React Hooks
  3. Understanding React Hooks
  4. Built-in Hooks: useState, useEffect and useReducer
  5. Built-in Hooks: useRef, useContext, useMemo and useCallback
  6. Context and Custom Hooks for a Redux-like Experience
  7. React Hooks and Context for Modal Popup Forms
  8. Improving Performance with useMemo, memo and useCallback
  9. Improving UX with useDeferredValue and useTransition
  10. Managing Forms with useActionState and useFormStatus
  11. Course Summary
  12. 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 HooksWith Hooks
Class components for stateFunction components are sufficient
this.setState hard to understanduseState is intuitive
Fragmented lifecycle methodsuseEffect is unified
Complex logic reuseSimple custom hooks

Hook Benefits

  1. Reusable logic: custom hooks allow sharing state logic
  2. Separation of concerns: decoupling between logic and rendering
  3. 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-vitals configuration in .eslintrc.json enables 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 useState call 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:

FeatureuseContextuse
Conditional call❌ Forbidden✅ Allowed
Suspense support
Promises support
React version16.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

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

ToolTypeUsageRe-render Avoided?
useMemoHookMemoizes result of an expensive computationYes (computation)
useCallbackHookMemoizes a function referenceYes (stable reference)
memoHOCPrevents re-render if props unchangedYes (component)
React CompilerToolAutomatically inserts the 3 aboveAutomatic

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

CriterionuseDeferredValueuseTransition
Source code controlNot requiredRequired
isPending availableVia comparison value !== deferred✅ Directly returned
Loading indicatorManual implementation✅ Native with isPending
Typical use caseValue that “follows” anotherControlled state update
React version18+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

ModuleHook(s)Key Concept
3useState, useEffectFundamentals, rules of hooks
4useState, useEffect, useReducerState management patterns
5useRef, useContext, use, useMemo, useCallbackDOM access, data sharing
6useContext + custom hooksRedux-like architecture
7useContextModal dialogs
8useMemo, useCallback, memoPerformance optimization
9useDeferredValue, useTransitionUI responsiveness
10useActionState, useFormStatusForms + Server Functions

12. Hook Reference Tables

All React Hooks — Complete Reference

HookImportReturnsPrimary Usage
useStatereact[state, setter]Local component state
useEffectreactvoidSide effects, lifecycle
useContextreactcontextValueRead a Context
useReducerreact[state, dispatch]Complex / multiple state
useRefreact{ current: T }DOM reference or persistent value
useMemoreactmemoizedValueMemoize an expensive computation
useCallbackreactmemoizedFnStable function reference
useLayoutEffectreactvoidSide effect before DOM paint
useImperativeHandlereactvoidImperative API with forwardRef
useDebugValuereactvoidLabel in React DevTools
useIdreactstringUnique ID for accessibility
useTransitionreact[isPending, startTransition]Low priority on state
useDeferredValuereactdeferredValueValue with low priority
usereactcontextValue | promiseResultConditional context + Suspense
useActionStatereact[state, formAction, isPending]Forms + server functions
useFormStatusreact-dom{ pending, data, method, action }Last submit status
useOptimisticreact[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

PitfallBad PracticeGood Practice
Direct state mutationstate.items.push(x)setState([...state.items, x])
Conditional hookif (cond) { useState() }Always at top level
Missing dependenciesuseEffect(() => f(x), [])useEffect(() => f(x), [x])
Object in depsuseEffect(() => {}, [{}])Memoize with useMemo
Forgetting cleanupaddEventListener without removeEventListenerAlways return cleanup fn
useContext too highContext at root → all re-renderPlace as low as possible
useFormStatus outside formCalled 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

Interested in this course?

Contact us to book it or get a custom training plan for your team.