React 19 Fundamentals: Complete Technical Reference

React Tutorial


What is React?

Overview

React is a JavaScript library created by Meta and maintained by the React Foundation (governed by a consortium of companies). It is a library for building user interfaces, allowing developers to write reusable components that can be efficiently displayed and updated when something changes.

Core Concepts

Component-Based Architecture

React enables developers to write reusable components that manage their own state and efficiently update when data changes. This eliminates the need for manual DOM manipulation using document.querySelector or addEventListener.

Example Syntax

React uses a declarative syntax to display dynamic data:

<p>You have {items.length} item(s)</p>

When the items array changes (e.g., from 4 to 5 items), React automatically updates the displayed text from "You have 4 item(s)" to "You have 5 item(s)".

Component Reusability

Components can be reused in multiple places throughout a website, promoting code maintainability and reducing duplication.

React's Scope and Limitations

Not a UI Library

React does not provide pre-designed UI elements such as buttons or cards. It manages complicated UI logic but does not include a design system. Developers must use CSS or choose a separate design library for styling.

Library vs Framework Equivalence

What is possible with React is also possible with other libraries or frameworks. React does not enable unique functionality that cannot be achieved elsewhere.

React's Evolution

React has evolved from a small library into a comprehensive ecosystem:

  • Originally a client-side library
  • Now supports server-side rendering
  • Recommended to be used with frameworks such as Next.js or React Router/Remix
  • Can now run on the server via React Server Components

Learning Approach

When learning React, it is important to understand the library separately before integrating it with frameworks. The official React docs recommend using a framework for production applications, but learning the core library first provides a stronger foundation.

React and TypeScript

TypeScript is recommended for production React applications if developers already have TypeScript experience. However, when learning React, it is easier to focus on React's concepts without the added complexity of TypeScript.

Summary

  • React is a library for building user interfaces
  • React enables reusable components that efficiently update when data changes
  • React does not provide design or UI elements
  • React has evolved to support server-side rendering and is often used within frameworks
  • Learning React as a standalone library before using frameworks provides better understanding
  • Learning React without TypeScript initially simplifies the learning process

The react Package

Overview

The react package (lowercase r) is the core library that must be installed and imported to use React. React is not part of the browser and must be explicitly imported in JavaScript files where it is needed.

File Extensions

React projects can use either .js or .jsx file extensions (.tsx if using TypeScript). The choice typically depends on the build tool being used:

  • Vite uses .jsx file extensions by default
  • There is no functional difference between .js and .jsx in React projects

Module System

Every JavaScript file is a standalone module. Variables, functions, and imports in one file are isolated and do not affect other files.

Importing React

React must be imported at the top of files where it is needed:

import React from 'react'

The React object is exported as a default value, which allows the import React syntax.

Alternative Import Syntax

Specific exports can be imported directly using named imports:

import { version } from 'react'

The React Object

The imported React object contains several methods and properties:

  • Method: A function that must be called with parentheses
  • Property: A value that is often pre-calculated

React.version

React exposes its current version through the version property.

Example: Default Import

import React from 'react'

console.log(React.version) // "19.0.0"

Example: Named Import

import { version } from 'react'

console.log(version) // "19.0.0"

Import Cost

Every import statement adds code to the application bundle. The react library weighs approximately 7KB when imported.

Monitoring Import Size

Developers using VSCode can install the Import Cost extension to see the size of imports directly in the editor.

Summary

  • Import React in every file where it is needed: import React from "react"
  • Access the current React version with React.version
  • The react package weighs approximately 7KB
  • Every import adds to the application bundle size

document.createElement

Overview

document.createElement(tagName) is a DOM API provided by browsers that allows programmatic creation of HTML elements. Understanding this API provides context for React's React.createElement() method, which is inspired by it.

Basic Usage

Creating an Element

// This creates: <h2></h2>
const element = document.createElement('h2')

// Which you can then insert in the DOM:
document.body.appendChild(element)

DOM Properties

HTML elements created with document.createElement() have numerous properties that can be accessed and modified. Use console.dir(element) to view all properties of an element.

Key Properties

The most important properties for HTML elements are:

  • id
  • style
  • className

Setting Styles

const element = document.createElement('h2')
element.style = 'color: red; background-color: blue'

Setting Classes

const element = document.createElement('h2')
element.className = 'text--regular color--primary'

className vs class

The property name is className rather than class because class is a reserved keyword in JavaScript used for defining JavaScript classes (which can be instantiated with the new operator).

Alternative: classList.add()

Classes can also be added using:

element.classList.add('name-of-class')

However, in React, directly manipulating the DOM should be avoided. Instead, define the elements to be rendered declaratively.

Summary

  • React.createElement() draws parallels with document.createElement()
  • document.createElement(tagName) creates an HTML element
  • element.className = "text--regular color--primary" sets classes on the element
  • className is used instead of class because class is a reserved JavaScript keyword

React.createElement

Overview

React.createElement() is React's method for creating elements. Unlike document.createElement(), which creates DOM elements, React.createElement() returns an object representing a DOM element.

Usage

Importing React

import React from 'react'

// We can now use React.createElement()

Comparison with document.createElement()

Return Value

document.createElement(): Returns a DOM element that can be inserted into the DOM.

React.createElement(): Returns an object that represents the DOM element.

Example

const element = React.createElement('h1')
console.log(element)

Output (simplified):

{
  type: 'h1',
  props: {}
}

Virtual DOM

React.createElement() returns an object rather than a DOM element because React operates a Virtual DOM. A virtual DOM is a representation of the UI kept in memory and synced with the actual DOM. The main goal is to check for updates in memory and update the DOM only when necessary.

The returned object is an internal representation and should not be modified directly.

Setting Properties

className and id

DOM properties work the same way as with document.createElement(). Use className instead of class:

React.createElement('h1', { className: 'center', id: 'headline' })

Children (Text Content)

To add text inside an element, provide the third parameter called children:

React.createElement('h1', {}, 'Hello World')

This returns an object representing an h1 element containing "Hello World".

If no properties are needed, pass {} or null as the second parameter:

React.createElement('h1', null, 'Hello World')

JSX Alternative

The syntax React.createElement("h1", {}, "Hello World") can feel verbose. JSX simplifies this to <h1>Hello World</h1>. However, JSX is not exactly the same as HTML, so understanding React.createElement() is important before using JSX.

What is a React Element?

A React Element is the smallest building block of a User Interface. It represents what the smallest piece of the UI will look like.

In its simplest form, a React Element could be a paragraph with text:

React.createElement('p', {}, 'Welcome')

This represents: <p>Welcome</p>

Multiple React Elements are combined into Components, and multiple Components are combined into an interactive application.

Function Signature

React.createElement(type, options, children)
  • type: The HTML tag name (e.g., "h1", "p", "div")
  • options: An object with properties like className, id, etc.
  • children: The content inside the element

Example

React.createElement('h1', { className: 'text--regular' }, 'Welcome!')

This creates an object describing:

<h1 class="text--regular">Welcome!</h1>

Summary

  • A React Element is the smallest building block of a User Interface
  • React.createElement() returns an object called a React Element
  • Function signature: React.createElement(type, options, children)
  • React.createElement("h1", {className: "text--regular"}, "Welcome!") describes <h1 class="text--regular">Welcome!</h1>
  • A virtual DOM is a representation of the UI kept in memory and synced with the actual DOM
  • The virtual DOM checks for updates in memory and updates the DOM only when necessary

React Fundamentals — Technical Summary

Summary

  • React is a library for building user interfaces
  • React enables reusable components that efficiently update when data changes
  • React provides no built-in design system or UI elements
  • React evolved from a library into an ecosystem supporting both client and server execution
  • React library should be learned independently before adopting frameworks
  • TypeScript integration is optional; learning React first without TypeScript reduces complexity
  • Import React with: import React from "react"
  • Check React version with: React.version
  • React library size: 7KB (imported)
  • React.createElement() parallels document.createElement()
  • const element = document.createElement(tagName) creates HTML elements
  • Multiple CSS classes: element.className = "text--regular color--primary"
  • React Element: smallest UI building block
  • React.createElement() returns a React Element object
  • Function signature: React.createElement(type, options, children)
  • Example: React.createElement("h1", {className: "text--regular"}, "Welcome!") describes <h1 class="text--regular">Welcome!</h1>
  • Virtual DOM: in-memory UI representation synced with actual DOM
  • Virtual DOM purpose: check updates in memory, minimize DOM operations

ReactDOM

Overview

ReactDOM is the bridge between React's virtual representation of the UI and the browser's actual DOM. React creates a virtual DOM representation, while ReactDOM efficiently synchronizes this virtual representation with the browser DOM.

Core Concepts

React vs ReactDOM Separation

React and ReactDOM are separate libraries:

  • React: Core library for building UI components
  • ReactDOM: Binds React to browser DOM
  • React Native: Binds React to native mobile platforms (iOS/Android)

This separation enables React's core concepts to be reused across different rendering targets.

Virtual DOM

The Virtual DOM is an in-memory representation of the UI that React maintains. ReactDOM receives this representation and syncs it with the actual browser DOM.

Reconciliation

Reconciliation is the process by which ReactDOM syncs the Virtual DOM to the actual DOM. This process:

  • Updates only DOM elements that changed
  • Batches multiple changes together
  • Applies optimization techniques to minimize DOM operations

Key Points

  • ReactDOM handles efficient DOM updates
  • Only modified elements are updated in the DOM
  • Changes are batched for performance
  • Immutability is required for reconciliation to work correctly

Summary

  • ReactDOM connects React to the browser DOM
  • React library is platform-agnostic
  • ReactDOM makes React UI visible in browsers
  • React Native makes React UI visible in native apps
  • Reconciliation syncs Virtual DOM to actual DOM
  • Virtual DOM is the in-memory UI representation

ReactDOM Usage

Overview

ReactDOM provides APIs to render React elements in the browser. The react-dom package exports separate Client and Server APIs. This lesson covers the Client APIs used for browser-based React applications.

Core Concepts

Package Structure

The react-dom package exports two API categories:

  • Client APIs: For browser rendering
  • Server APIs: For server-side rendering

Importing ReactDOM

Import the createRoot function from the client package:

import { createRoot } from 'react-dom/client'

Important: Include /client after react-dom in the import path.

Import Cost

  • ReactDOM: 138KB
  • React + ReactDOM total: 145KB (7KB + 138KB)

Application Root

The root element is the DOM location where React renders the application. Typically a <div> with an id attribute:

<div id="root"></div>

Usage

Rendering Elements

Complete example of rendering a React element:

import React from 'react'
import { createRoot } from 'react-dom/client'

const paragraph = React.createElement('p', {}, 'Hello World')

const root = document.querySelector('#root')
createRoot(root).render(paragraph)

Process Breakdown

  1. Import React object and createRoot function
  2. Create React element using React.createElement
  3. Select root DOM element with document.querySelector()
  4. Call createRoot(root) to specify render location
  5. Call .render(element) to render the element
  6. Element appears in browser

Deprecated API (React 19)

The following approach no longer works in React 19:

// ❌ Removed in React 19
import { render } from 'react-dom'
render(React.createElement('p', {}, 'Hello World'), root)

The direct render method has been removed.

Summary

  • ReactDOM Client APIs handle browser rendering
  • Import: import {createRoot} from "react-dom/client"
  • ReactDOM package size: 138KB
  • Root element defines the render location
  • createRoot(root).render(element) renders elements to the DOM
  • Direct render() method removed in React 19

Root Element

Overview

The root element passed to ReactDOM becomes completely managed by ReactDOM. Once ReactDOM controls a root element, no other JavaScript code should modify its content directly.

Core Concepts

ReactDOM Management

When you designate an element as the root:

<div id="root"></div>
import React from 'react'
import { createRoot } from 'react-dom/client'

const element = React.createElement('h1', {}, 'Hello World')

const root = document.querySelector('#root')
createRoot(root).render(element)

ReactDOM takes complete control of the root element and its contents. All updates are handled by ReactDOM.

Single Root Creation

createRoot(root) should only be called once per root element. React manages all subsequent updates automatically through components, hooks, and state.

Use Cases

Single-Page Applications

Applications built entirely with React typically have one root element. The entire application renders inside this single root.

React Integration

When integrating React into existing applications built with other technologies (e.g., Ruby on Rails, Laravel), you can create multiple root elements. Each root represents a React-powered feature within the larger application.

Example: A shopping cart feature in an existing site might use:

<div id="react-cart"></div>

Applications integrating React for specific features may contain multiple root elements.

Summary

  • ReactDOM completely manages root elements
  • Do not directly modify root element content
  • createRoot() should be called once per root
  • Single-page React apps use one root element
  • Integrated React features may require multiple roots
  • React handles all updates automatically

ReactDOM and Root Element — Technical Summary

Summary

  • ReactDOM connects React to the browser DOM
  • Reconciliation syncs Virtual DOM to actual DOM
  • Virtual DOM is an in-memory UI representation
  • ReactDOM Client APIs handle browser rendering
  • Import: import {createRoot} from "react-dom/client"
  • ReactDOM package size: 138KB
  • Root element defines the render location
  • createRoot(root).render(element) renders elements to the DOM
  • ReactDOM completely manages root elements
  • Do not directly modify root element content
  • Single-page React apps typically use one root element
  • Integrated React features may use multiple roots

Key Points

  • React and ReactDOM are separate libraries for different rendering targets
  • ReactDOM optimizes DOM updates through batching and selective rendering
  • Root element creation should occur once per root
  • React handles all subsequent updates automatically

Vite Project Setup

Overview

Vite is a modern build tool and development server for web projects. It provides fast development experience and optimized production builds for React applications.

Core Concepts

Framework Recommendations

The official React documentation recommends using React with frameworks (e.g., Next.js). However, learning React as a standalone library first provides better foundation before adopting frameworks or TypeScript.

Prerequisites

Vite requires Node.js and npm:

  • Download from nodejs.org
  • npm installs automatically with Node.js
  • Verify installation: node -v

Usage

Creating a Vite React Project

Automated template selection:

npm create vite@latest react-tutorial -- --template react

This command:

  • Creates new project in react-tutorial folder
  • Automatically selects React template

Interactive setup (alternative):

npm create vite@latest

Prompts for:

  • Project name
  • Framework choice
  • TypeScript support

TypeScript Consideration

For React beginners, learning React concepts without TypeScript reduces complexity. TypeScript can be adopted after mastering core React concepts.

Deprecated Tool

create-react-app is deprecated. Migrate existing projects to Vite.

Project Structure

The generated boilerplate includes concepts not yet covered. The main.js file demonstrates JSX usage.

Summary

  • Vite is a modern build tool for web projects
  • Creates React projects with optimized development environment
  • Command: npm create vite@latest react-tutorial -- --template react
  • Requires Node.js and npm
  • TypeScript is optional for beginners
  • create-react-app is deprecated in favor of Vite

Editor Setup

Overview

VS Code is the recommended text editor for React development, with specific extensions that enhance productivity and code quality.

Recommended Extensions

Import Cost

Displays the size of imported packages inline, helping track bundle size.

Prettier - Code Formatter

Automatically formats code according to consistent rules.

Configuration:

  1. Open Settings (cmd + , on Mac, or Ctrl + Shift + P → "Open Settings UI")
  2. Search for "Format on save"
  3. Enable "Editor: Format on save"

ESLint

Checks code for bugs and style issues.

Vite Integration: ESLint works automatically with Vite React projects, which include pre-configured ESLint settings.

Summary

  • VS Code is the recommended editor
  • Install extensions: Import Cost, Prettier, ESLint
  • Enable "Format on save" for Prettier
  • ESLint pre-configured in Vite React projects
  • Linting helps catch bugs during development

JSX Introduction

Overview

JSX is a syntax extension for JavaScript that provides a more readable way to define React elements. While it resembles HTML, JSX is transformed into React.createElement() calls.

Core Concepts

JSX as Syntactic Sugar

JSX:

const title = <h1>Hello World</h1>

Transforms to:

const title = React.createElement('h1', {}, 'Hello World')

JSX makes UI code more readable and concise compared to verbose createElement() calls.

Browser Compatibility

Browsers cannot execute JSX directly. Build tools (like Babel) transform JSX into standard JavaScript. React project tools (Vite, create-react-app) handle this transformation automatically.

JSX Transformer Evolution

Pre-React 17: Required explicit React import:

import React from 'react'

React 17+: New JSX transformer eliminates the import requirement. Babel plugin transforms JSX to internal runtime:

import { jsx as _jsx } from 'react/jsx-runtime'

const title = _jsx('h1', {
  children: 'Hello World',
})

Usage

Basic JSX Syntax

const element = <h1>Hello World</h1>

JSX with ReactDOM

Before JSX:

import React from 'react'
import { createRoot } from 'react-dom/client'

const root = document.querySelector('#root')
createRoot(root).render(React.createElement('h1', {}, 'Hello World'))

With JSX:

import { createRoot } from 'react-dom/client'

const root = document.querySelector('#root')
createRoot(root).render(<h1>Hello World</h1>)

Examples

Babel REPL

Test JSX transformation at babeljs.io/repl. Configure React Runtime to "Classic" to see React.createElement() output instead of internal runtime.

Summary

  • JSX is syntactic sugar for React.createElement()
  • JSX resembles HTML but is not HTML
  • Browsers require JSX transformation to JavaScript
  • Build tools (Babel, Vite) handle JSX transformation
  • React 17+ eliminates need for explicit React import
  • JSX significantly improves code readability

JSX Children

Overview

JSX supports nested elements, allowing components to contain child elements similar to HTML structure.

Core Concepts

Children in JSX

HTML structure:

<div class="container">
  <h1>Hello World</h1>
</div>

JSX equivalent:

const element = (
  <div className="container">
    <h1>Hello World</h1>
  </div>
)

React.createElement() equivalent:

const element = React.createElement(
  'div',
  {
    className: 'container',
  },
  React.createElement('h1', null, 'Hello World'),
)

Multiple Children

HTML list:

<ul>
  <li>Apple</li>
  <li>Banana</li>
  <li>Watermelon</li>
</ul>

JSX equivalent:

const list = (
  <ul>
    <li>Apple</li>
    <li>Banana</li>
    <li>Watermelon</li>
  </ul>
)

React.createElement() equivalent:

const list = React.createElement(
  'ul',
  null,
  React.createElement('li', null, 'Apple'),
  React.createElement('li', null, 'Banana'),
  React.createElement('li', null, 'Watermelon'),
)

createElement Function Signature with more than one child

createElement(type, props, ...children)

The ...children parameter uses JavaScript rest parameters to accept unlimited child arguments as an array.

Summary

  • JSX represents elements with nested children
  • Third argument of React.createElement() accepts child elements or text
  • Rest parameters (...children) capture multiple child elements
  • JSX syntax significantly improves readability over createElement() calls
  • Function signature: createElement(type, props, ...children)

Self-Closing Tags

Overview

JSX requires all tags to be explicitly closed, including elements that are self-closing in HTML.

Core Concepts

HTML Self-Closing Elements

Elements that cannot contain children:

  • img
  • br (line break)
  • hr (horizontal ruler)
  • input
  • link
  • meta
  • Lesser-used: area, base, col, embed, source, track, wbr

JSX Closing Requirements

All tags must be closed in JSX. Two valid approaches:

Explicit closing tags:

const lineBreak = <br></br>
const image = <img src="..." alt=""></img>

Self-closing syntax (recommended):

const lineBreak = <br />
const image = <img src="..." alt="" />

Children Restriction

Self-closing tags cannot have children. Attempting to add children causes error:

br is a void element tag and must neither have children nor use dangerouslySetInnerHTML.

Usage

Self-Closing Syntax

Add / before closing >:

<br />
<img src="..." alt="" />
<input type="text" />

This syntax is recommended for self-closing elements and will be revisited with components.

Summary

  • HTML allows unclosed tags for certain elements
  • JSX requires all tags to be closed
  • Self-closing syntax: <element />
  • Self-closing tags cannot have children
  • Recommended for: img, br, hr, input, link, meta

JSX Fundamentals — Technical Summary

Summary

  • JSX is syntactic sugar for React.createElement()
  • JSX resembles HTML but transforms to JavaScript
  • Build tools (Babel, Vite) handle JSX transformation automatically
  • JSX supports nested child elements
  • Third argument of React.createElement() accepts children
  • Rest parameters (...children) capture multiple child elements
  • Function signature: createElement(type, props, ...children)
  • All JSX tags must be closed explicitly
  • Self-closing syntax recommended: <element />
  • Self-closing tags cannot contain children
  • Common self-closing elements: img, br, hr, input, link, meta

Key Points

  • React 17+ eliminates need for explicit React import
  • JSX improves code readability over createElement() calls
  • Self-closing syntax applies to both elements and components

React Components

Overview

A React component is a reusable, self-contained function that returns a React element describing a section of the User Interface.

Core Concepts

Component Definition

Components are functions that return React elements:

function Footer() {
  return (
    <div className="footer-wrapper">
      <p>Company name</p>
      <p>All rights reserved</p>
    </div>
  )
}

Component Usage

Use components in JSX like elements:

import { createRoot } from 'react-dom/client'

function Footer() {
  return (
    <div>
      <h3>Company name</h3>
      <p>All rights reserved</p>
    </div>
  )
}

const root = document.querySelector('#root')
createRoot(root).render(<Footer></Footer>)

Naming Convention

Component names must use UpperCamelCase with first character uppercase:

Valid examples:

  • Footer
  • ChatMessage
  • Button

First character must be uppercase to be recognized as a component.

Benefits

Components provide:

  • Code reusability
  • Isolated, independent UI pieces
  • Easier debugging and maintenance
  • Simplified UI management

Summary

  • React components are functions returning React elements
  • Components are reusable, self-contained UI pieces
  • Component names require UpperCamelCase format
  • First character must be uppercase
  • Components promote code reuse and modularity
  • Components are the fundamental building blocks of React UIs

Components Under the Hood

Overview

React distinguishes between components and elements based on the first character of the tag name. Understanding this distinction is crucial for proper component usage.

Core Concepts

Component vs Element Recognition

React's identification process:

  1. Check first character of JSX tag
  2. Uppercase letter → Component
  3. Lowercase letter → DOM element

Example:

  • <Navbar></Navbar> → Component (uppercase N)
  • <navbar></navbar> → DOM element (lowercase n)

Lowercase component names render as HTML tags, even if invalid.

Self-Closing Syntax

Components support self-closing syntax:

<Navbar />

Equivalent to:

<Navbar></Navbar>

JSX Transformation

JSX to createElement():

const element = <Navbar />

Transforms to:

const element = React.createElement(Navbar, {})

Important: Pass function reference (without parentheses), not function call. React calls components internally.

Infinite Loop Warning

Avoid recursive component rendering:

function Button() {
  // ❌ Infinite loop
  return <Button>Click me</Button>
}

This creates infinite recursion because <Button> calls the Button function repeatedly.

Solution: Use lowercase for HTML elements:

function Button() {
  // ✅ Correct
  return <button>Click me</button>
}

Summary

  • Component vs element determined by first character case
  • Components: uppercase first character
  • Elements: lowercase first character
  • Components support self-closing syntax: <Component />
  • <Navbar />React.createElement(Navbar, {})
  • Pass function reference, not function call
  • Avoid recursive component rendering to prevent infinite loops
  • Lowercase tags render as DOM elements

JSX and Automatic Semicolon Insertion

Overview

When placing a newline between return and JSX, JavaScript's Automatic Semicolon Insertion (ASI) can cause components to return undefined.

Core Concepts

The Problem

Incorrect formatting:

function Navbar() {
  return
  ;<div className="navbar">
    <h1>SuperM</h1>
    <p>Online shopping simplified</p>
  </div>
}

JavaScript transforms this to:

function Navbar() {
  return // Semicolon auto-inserted
  React.createElement(
    'div',
    {
      className: 'navbar',
    },
    React.createElement('h1', null, 'SuperM'),
    React.createElement('p', null, 'Online shopping simplified'),
  )
}

The function returns undefined because ASI inserts a semicolon after return.

Automatic Semicolon Insertion

ASI is a JavaScript feature that automatically inserts semicolons after specific statements:

  • let/const variable declarations
  • import/export statements
  • return statements

Solution

Wrap JSX with parentheses to prevent ASI:

function Navbar() {
  return (
    <div className="navbar">
      <h1>SuperM</h1>
      <p>Online shopping simplified</p>
    </div>
  )
}

Opening parenthesis after return signals JavaScript that the expression continues, preventing automatic semicolon insertion.

Prettier Integration

Prettier code formatter automatically applies this fix, preventing ASI issues.

Summary

  • Newline between return and JSX triggers ASI
  • ASI inserts semicolon, causing undefined return
  • ASI is a JavaScript feature, not specific to React/JSX
  • Wrap JSX with parentheses () to prevent ASI
  • Prettier automatically handles this formatting

React Fragments

Overview

Fragments enable components to return multiple elements without adding extra DOM nodes. This solves JSX's limitation of returning only one element per component.

Core Concepts

The Limitation

Components cannot return multiple sibling elements directly:

function Landing() {
  const h1 = React.createElement('h1', null, 'Welcome to SuperM')
  const p = React.createElement('p', null, 'Online shopping simplified')
  // Cannot return both elements
}

Why Not Use div?

Wrapping with div works but creates problems:

  • Breaks Flex/Grid layouts
  • Adds unnecessary DOM complexity
  • Decreases DOM performance

Fragment Solution

Fragments wrap multiple elements without adding DOM nodes.

Three equivalent syntaxes:

Full syntax:

import React from 'react'

function Landing() {
  return (
    <React.Fragment>
      <h1>Welcome to SuperM</h1>
      <p>Online shopping simplified</p>
    </React.Fragment>
  )
}

Named import:

import { Fragment } from 'react'

function Landing() {
  return (
    <Fragment>
      <h1>Welcome to SuperM</h1>
      <p>Online shopping simplified</p>
    </Fragment>
  )
}

Short syntax (recommended):

function Landing() {
  return (
    <>
      <h1>Welcome to SuperM</h1>
      <p>Online shopping simplified</p>
    </>
  )
}

Generated HTML

All three produce identical HTML without extra wrapper:

<h1>Welcome to SuperM</h1>
<p>Online shopping simplified</p>

Transformation

Short syntax transforms to:

function Landing() {
  return React.createElement(
    React.Fragment,
    null,
    React.createElement('h1', null, 'Welcome to SuperM'),
    React.createElement('p', null, 'Online shopping simplified'),
  )
}

Usage

Combining with Parentheses

Prevent ASI by combining fragments with parentheses:

function Landing() {
  return (
    <>
      <h1>Welcome to SuperM</h1>
      <p>Online shopping simplified</p>
    </>
  )
}

When to Use

Only use fragments when returning multiple sibling elements. Single element with children doesn't need fragments.

Summary

  • JSX requires single root element per component
  • Fragments wrap multiple elements without adding DOM nodes
  • Three syntaxes: <React.Fragment>, <Fragment>, <> (recommended)
  • Short syntax requires no imports
  • Combine with parentheses to prevent ASI
  • Use only when necessary (multiple siblings)

React Components — Technical Summary

Summary

  • React components are functions returning React elements
  • Components are reusable, self-contained UI pieces
  • Component names require UpperCamelCase format
  • First character must be uppercase for component recognition
  • Components promote code reuse and modularity
  • Newline between return and JSX triggers ASI (Automatic Semicolon Insertion)
  • ASI inserts semicolon, causing undefined return
  • Wrap JSX with parentheses () to prevent ASI
  • Prettier automatically handles ASI formatting
  • JSX requires single root element per component
  • Fragments wrap multiple elements without adding DOM nodes
  • Fragment syntaxes: <React.Fragment>, <Fragment>, <> (recommended)
  • Combine fragments with parentheses to prevent ASI

Key Points

  • Components enable UI composition and reusability
  • Uppercase naming convention distinguishes components from elements
  • Fragments solve multi-element return limitation without DOM pollution
  • Short fragment syntax (<>) requires no imports

Component File Organization

Overview

Organizing components into separate files enables scalable application architecture. Each component in its own file improves maintainability and readability.

Core Concepts

File Structure

Before (single file):

// index.jsx
import { createRoot } from 'react-dom/client'

function Product() {
  return (
    <div className="product">
      <p>PineApple</p>
    </div>
  )
}

function App() {
  return (
    <div className="products-grid">
      <Product />
      <Product />
    </div>
  )
}

createRoot(document.querySelector('#root')).render(<App />)

After (separate files):

// Product.jsx
export default function Product() {
  return (
    <div className="product">
      <p>PineApple</p>
    </div>
  )
}
// index.jsx
import { createRoot } from 'react-dom/client'
import Product from './Product.jsx'

function App() {
  return (
    <div className="products-grid">
      <Product />
      <Product />
    </div>
  )
}

createRoot(document.querySelector('#root')).render(<App />)

Benefits

  • Improved readability
  • Self-contained, short files
  • Easier debugging (direct file-to-component mapping)
  • Better scalability

Usage

File Naming Convention

Use UpperCamelCase matching component name:

  • Component: Product
  • File: Product.jsx

This is the most common React convention. Maintain consistency within projects.

Import Path

Import path must match filename exactly:

import Product from './Product.jsx'

Use ./ prefix for local files (distinguishes from node modules).

File Location

In Vite projects, place component files in src/ folder alongside index.jsx.

Default Exports

Use default exports for components:

// ✅ Named export (recommended)
export default function Product() { ... }

// ❌ Avoid anonymous
export default function() { ... }

Named exports enable better debugging with function names in DevTools console.

Named vs Default Exports

  • Default exports: Recommended for components
  • Named exports: Use only with strong justification

Import syntax for default exports (no curly braces):

import ComponentName from './ComponentName.jsx'

Summary

  • One component per file improves scalability
  • Use export default for components
  • Import: import ComponentName from "./ComponentName.jsx"
  • File naming: UpperCamelCase matching component name
  • Place files in src/ folder (Vite projects)
  • Always name default exports for debugging
  • Follow existing project conventions

React StrictMode

Overview

StrictMode is a development tool that helps identify potential problems in React applications. It activates additional checks and warnings during development without affecting production builds.

Core Concepts

Purpose

StrictMode helps find:

  • Common bugs in components
  • Unsafe lifecycle methods
  • Unexpected side effects
  • Deprecated APIs

Development-Only Tool

  • Active only in development mode
  • No visual changes
  • No production performance impact
  • Helps catch issues early

Usage

Basic Implementation

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'

function App() {
  return <h1>My app</h1>
}

const root = document.querySelector('#root')
createRoot(root).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

Implementation steps:

  1. Import StrictMode from "react"
  2. Wrap main component with <StrictMode>...</StrictMode>

Alternative Syntax

Using React object:

import React from 'react'
import { createRoot } from 'react-dom/client'

function App() {
  return <h1>My app</h1>
}

const root = document.querySelector('#root')
createRoot(root).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

Both syntaxes are functionally identical.

Behavior

Development Mode

  • Components render additional times to detect bugs
  • console.log statements may appear twice
  • Extra checks run to identify issues

Production Mode

  • StrictMode renders children normally
  • No extra rendering
  • No performance overhead

Summary

  • StrictMode helps find bugs during development
  • Import from "react" and wrap application root
  • No visual changes in UI
  • No production performance impact
  • Development-only behavior
  • Components may render multiple times in development
  • Two syntaxes available: <StrictMode> or <React.StrictMode>

Component Organization — Technical Summary

Summary

  • One component per file improves application scalability
  • Use export default for component exports
  • Import syntax: import ComponentName from "./ComponentName.jsx"
  • File naming convention: UpperCamelCase matching component name
  • Place component files in src/ folder (Vite projects)
  • Always name default exports for debugging clarity
  • Use ./ prefix for local file imports
  • Follow existing project naming conventions
  • StrictMode helps identify bugs during development
  • Import StrictMode from "react" and wrap application root
  • StrictMode has no visual UI changes
  • StrictMode has no production performance impact
  • StrictMode is development-only feature
  • Components may render multiple times in development mode

Key Points

  • Separate files enable easier debugging with direct file-to-component mapping
  • Default exports recommended over named exports for components
  • StrictMode activates additional development checks without affecting production
  • Consistent naming conventions crucial for project maintainability

JSX Expressions

Overview

JSX expressions enable dynamic content in React components by embedding JavaScript code that resolves to values. Expressions are wrapped in curly braces {} and evaluated at runtime.

Core Concepts

Expression Definition

An expression is any valid JavaScript code that resolves to a value:

  • Arithmetic: 3 + 47
  • Strings: "Sam""Sam"
  • Objects: new Date() → Date object
  • Variables: name → variable's value

Expression Syntax

Wrap expressions in curly braces within JSX:

function Notifications() {
  return <h1>You have {2 + 3} notifications</h1>
}

Result: <h1>You have 5 notifications</h1>

Usage

Variable Expressions

function Welcome() {
  const user = {
    id: 1,
    name: 'Sam',
  }

  return <p>Welcome {user.name}!</p>
}

Function Call Expressions

function capitalise(word) {
  return word[0].toUpperCase() + word.substring(1).toLowerCase()
}

function Welcome() {
  const name = 'brendan'

  return <p className="user-info">Welcome {capitalise(name)}</p>
}

Result: <p className="user-info">Welcome Brendan</p>

Summary

  • Expressions are JavaScript code that resolves to a value
  • Use curly braces {} to embed expressions in JSX
  • Expressions work with variables, function calls, and any JavaScript code
  • Expressions are evaluated and replaced with their resulting value

Dynamic Properties

Overview

JSX properties accept dynamic values through JavaScript expressions. Use curly braces {} to pass expressions as property values instead of static strings.

Core Concepts

Expression Properties

Pass JavaScript expressions as property values using curly braces:

function App() {
  const value = 'highlighted'

  return <p className={value}>Hello World</p>
}

Result: <p class="highlighted">Hello World</p>

Conditional Properties

Use ternary operators for conditional property values:

function App() {
  const isHighlighted = false

  return <p className={isHighlighted ? 'highlighted' : 'normal'}>Hello World</p>
}

Result: <p class="normal">Hello World</p>

Quotes vs Expressions

String properties: Use quotes

<p className="static-value">Text</p>

Expression properties: Use curly braces

<p className={dynamicValue}>Text</p>

Incorrect: className="{"highlighted"}" (treated as literal string)

Dynamic Strings

Use template literals or concatenation within expressions:

function App() {
  const currentYear = new Date().getFullYear()

  // Template literal
  return <p className={`year-${currentYear}`}>Hello World</p>

  // Concatenation
  return <p className={'year-' + currentYear}>Hello World</p>
}

Usage Examples

function App() {
  const id = 'my-paragraph'
  const year = new Date().getFullYear()
  const isActive = true

  return (
    <p id={id} className={isActive ? 'active' : 'inactive'} data-year={year}>
      Hello World
    </p>
  )
}

Summary

  • Use curly braces {} for dynamic property values
  • Values starting with quotes are always treated as strings
  • Values starting with curly braces are always treated as expressions
  • Include dynamic content in strings using template literals or concatenation within expressions

JSX Expressions — Technical Summary

Summary

This chapter covered JSX expressions and dynamic properties.

Expressions:

  • Expression = JavaScript code that resolves to a value
  • Wrap expressions in curly braces {}
  • Works with variables, function calls, any JavaScript code
  • Expressions are evaluated and replaced with their value

Dynamic Properties:

  • Pass dynamic values to properties using curly braces {}
  • Values starting with quotes = always strings
  • Values starting with curly braces = always expressions
  • Include dynamic content in strings using template literals or concatenation within expressions

Props

Overview

Props (properties) enable data passing to React components, making them customizable and reusable. Props are passed as an object parameter to component functions.

Core Concepts

Props Mechanism

When using a component with props:

function App() {
  return <Welcome name="Sam" />
}

function Welcome(props) {
  console.log(props) // {name: "Sam"}
}

Process:

  1. <Welcome name="Sam" /> converts to React.createElement(Welcome, {name: "Sam"})
  2. React calls Welcome function with props object {name: "Sam"}
  3. Access props through the props parameter

Props Usage

function Welcome(props) {
    return <p>Hello {props.name}</p>;
}

// Usage
<Welcome name="Sam" /> // → <p>Hello Sam</p>
<Welcome name="Alex" /> // → <p>Hello Alex</p>

Multiple Props

function UserProfile(props) {
  console.log(props); // {username: "abc123"}
  return <p>You are logged in as {props.username}.</p>;
}

// Usage
<UserProfile username="abc123" />
<UserProfile username="samgreen" />

Props Immutability

Critical rule: Never modify props. Props are read-only.

Usage

function App() {
  return (
    <>
      <UserProfile username="abc123" />
      <UserProfile username="samgreen" />
    </>
  )
}

function UserProfile(props) {
  return <p>You are logged in as {props.username}.</p>
}

Summary

  • Props pass data to components as object parameter
  • Props make components reusable and customizable
  • First parameter of component function always receives props
  • Empty object {} when no props are passed
  • Never modify props (read-only)

Props — Advanced Usage

Overview

Props support multiple properties, various data types, and conditional rendering. Understanding proper prop types and JSX conversion enables more complex component behavior.

Core Concepts

Multiple Props

Pass multiple properties through the props object:

function UserProfile(props) {
  console.log(props) // {firstName: "Sam", lastName: "Green"}
  return (
    <h1>
      Hello {props.firstName} {props.lastName}.
    </h1>
  )
}

const element = <UserProfile firstName="Sam" lastName="Green" />

Numbers and Booleans

Incorrect: Quotes make them strings

function User(props) {
  console.log(props) // {age: "40", isAdmin: "false"} ❌
}

const element = <User age="40" isAdmin="false" />

Correct: Curly braces preserve types

function User(props) {
  console.log(props) // {age: 40, isAdmin: false} ✅
}

const element = <User age={40} isAdmin={false} />

Conditional Rendering with Props

Use if statements to render JSX conditionally based on props:

function User(props) {
  if (props.isAdmin) {
    return <h1>You are an Admin</h1>
  } else {
    return <h1>Hello User</h1>
  }
}

const element = <User isAdmin={false} />

Behind the scenes (React.createElement):

function User(props) {
  if (props.isAdmin) {
    return React.createElement('h1', null, 'You are an Admin')
  } else {
    return React.createElement('h1', null, 'Hello User')
  }
}

const element = React.createElement(User, { isAdmin: false })

Usage Examples

function User(props) {
  console.log(props)
  // {
  //   firstName: "Sam",
  //   lastName: "Green",
  //   age: 40,
  //   isAdmin: false
  // }

  if (props.isAdmin) {
    return (
      <h1>
        Admin: {props.firstName} {props.lastName}
      </h1>
    )
  }

  return (
    <p>
      User {props.firstName}, Age: {props.age}
    </p>
  )
}

const element = (
  <User firstName="Sam" lastName="Green" age={40} isAdmin={false} />
)

Summary

  • Props object can contain multiple properties
  • Always wrap numbers and booleans in curly braces {} to preserve their types
  • Use if statements for conditional JSX rendering based on props
  • JSX converts to React.createElement calls, enabling JavaScript logic

Props — Technical Summary

Summary

This chapter covered React props for component data passing and reusability.

Props Fundamentals:

  • Props pass data to components as object parameter
  • First parameter of component function receives props
  • Empty object {} when no props passed
  • Props are read-only (never modify)
  • Makes components reusable and customizable

Multiple Props:

  • Props object can contain multiple properties
  • Wrap numbers and booleans in {} to preserve types (not strings)
  • Strings use quotes, expressions use curly braces

Conditional Rendering:

  • Use if statements to conditionally render JSX based on props
  • JSX converts to React.createElement calls, enabling JavaScript logic

TypeScript Note:

  • React deprecated PropTypes
  • Modern approach uses TypeScript for prop type checking
  • For ESLint, disable react/prop-types rule in eslint.config.js

Arrays in JSX

Overview

JSX supports rendering arrays of elements using JavaScript's Array.map() method. This enables dynamic list generation from data arrays.

Core Concepts

Array Mapping

Transform array data into JSX elements using .map():

function App() {
  const names = ['Sam', 'Alex', 'Charley']

  return names.map((name) => <li>{name}</li>)
}

Result:

<li>Sam</li>
<li>Alex</li>
<li>Charley</li>

Key warning: React requires a key prop for list items (covered in next lesson).

Implicit Return

Arrow functions with single expressions can use implicit return:

// Implicit return (no braces, no return keyword)
names.map((name) => <li>{name}</li>)

// Explicit return (with braces, requires return)
names.map((name) => {
  return <li>{name}</li>
})

Implicit return requirements:

  1. Must be arrow function
  2. Single statement only (remove curly braces)
  3. Remove return keyword

Common mistake:

// ❌ Doesn't work - has braces but missing return
names.map((name) => {
  ;<li>{name}</li>
})

// ✅ Fixed - added return keyword
names.map((name) => {
  return <li>{name}</li>
})

Wrapping Lists

Assign mapped elements to variable, then wrap:

function App() {
  const names = ['Sam', 'Alex', 'Charley']

  // Store mapped elements
  const lists = names.map((name) => <li>{name}</li>)

  // Wrap with ul
  return <ul>{lists}</ul>
}

Result:

<ul>
  <li>Sam</li>
  <li>Alex</li>
  <li>Charley</li>
</ul>

Usage

function App() {
  const users = [
    { id: 1, name: 'Sam' },
    { id: 2, name: 'Alex' },
    { id: 3, name: 'Charley' },
  ]

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

Summary

  • Use Array.map() to render arrays in JSX
  • Wrap dynamic values in curly braces {}
  • Implicit return: arrow function with single expression (no braces/return)
  • Explicit return: use braces and return keyword
  • JSX elements can be assigned to variables
  • Each list item requires key prop (see next lesson)

React Keys in Lists

Overview

React requires unique key props for list items to optimize rendering performance. Keys enable React to track individual elements and update only what changed.

Core Concepts

Performance Problem

Without keys, React can't efficiently track list items:

// 1,000 podcasts - inefficient without keys
const podcasts = [{id: 1, title: "NPR"}, ...];

function App() {
    return <ul>{podcasts.map(podcast => <li>{podcast.title}</li>)}</ul>;
}

Issue: Hiding/updating one podcast requires re-rendering all 1,000 items because React can't identify which specific item changed.

Key Solution

Provide unique key prop to each list item:

const podcasts = [{id: 1, title: "NPR"}, ...];

function App() {
    return <ul>{podcasts.map(podcast =>
        <li key={podcast.id}>{podcast.title}</li>
    )}</ul>;
}

Benefit: React tracks each item individually. Deleting item 387 updates only that one element, not all 999 others.

Key Requirements

Must be unique within the list:

// ✅ Good - database IDs are unique
<li key={item.id}>{item.name}</li>

// ✅ Good - country names are unique
countries.map(country => <li key={country}>{country}</li>)

// ❌ Bad - duplicate keys cause warnings
<li key={1}>Item</li>
<li key={1}>Item</li>  // Warning!

Duplicate key warning:

Warning: Encountered two children with the same key. Keys should be unique so that components maintain their identity across updates.

What to Use as Keys

Best practice: Use database ID

items.map((item) => <li key={item.id}>{item.name}</li>)

Unique values: Use the value itself if naturally unique

const countries = ['Nigeria', 'Serbia', 'Latvia', 'Thailand']
countries.map((country) => <li key={country}>{country}</li>)

Avoid: Array index (changes when items reordered/deleted)

// ❌ Avoid - index changes between renders
items.map((item, index) => <li key={index}>{item}</li>)

Usage Example

function PodcastList() {
  const podcasts = [
    { id: 1, title: 'NPR', favorited: false },
    { id: 2, title: 'TED', favorited: true },
    { id: 3, title: 'BBC', favorited: false },
  ]

  return (
    <ul>
      {podcasts.map((podcast) => (
        <li key={podcast.id}>
          {podcast.title}
          {podcast.favorited && ' ⭐'}
        </li>
      ))}
    </ul>
  )
}

Summary

  • Keys must be unique within the same list
  • Use database IDs as keys when available
  • Keys optimize React's rendering by tracking individual items
  • Avoid using array index as key (changes between renders)
  • Keys don't change between renders for stable identification
  • Duplicate keys cause React warnings and inefficient updates

Summary

Arrays and JSX Generation:

  • Use Array.map() to generate JSX elements from arrays
  • Generated JSX can be wrapped with other JSX elements
  • Implicit return possible when function body is single statement (remove {} and return)

The key Prop:

  • React requires key prop for each child in list
  • Must be unique within same list
  • Should ideally map to database ID of item
  • Helps React optimize updates by tracking each child
  • Must be stable value that doesn't change between renders
  • Required for list rendering performance and correctness

Object and Array Props

Overview

React props support all JavaScript data types including arrays and objects. This enables passing complex data structures to components.

Core Concepts

Passing Arrays as Props

Arrays are passed using curly braces (JSX expression wrapper):

function App() {
  const postIds = [10, 159, 95]
  return <UserProfile postIds={postIds} />
}

Inline syntax:

<UserProfile postIds={[10, 159, 95]} />

The {} are for JSX expression, [] are for the array.

Passing Objects as Props

Objects require variable or inline syntax:

function App() {
  const data = { id: 1, username: 'alex' }
  return <UserProfile data={data} />
}

Inline syntax (double curly braces):

<UserProfile data={{ id: 1, username: 'alex' }} />

First {} = JSX expression, second {} = object literal.

Inline Styles

Styles are passed as objects to the style prop:

function App() {
  const styles = { color: 'red' }
  return <p style={styles}>Hello World</p>
}

Inline syntax:

<p style={{ color: 'red' }}>Hello World</p>

CSS Property Rules

CamelCase naming:

// ✅ Correct - camelCase
<p style={{backgroundColor: "green", textDecoration: "underline"}}>Text</p>

// ❌ Wrong - kebab-case doesn't work
<p style={{"background-color": "green"}}>Text</p>

Number values default to pixels:

// Results in font-size: 12px
<p style={{ fontSize: 12 }}>Text</p>

Other units require strings:

const factor = 1
;<p style={{ fontSize: factor + 'rem' }}>Text</p>

Usage

function ProductCard({ details, prices }) {
  return (
    <div
      style={{
        backgroundColor: 'white',
        padding: 16,
        borderRadius: 8,
      }}
    >
      <h2>{details.name}</h2>
      <p>{details.description}</p>
      <span style={{ color: 'green' }}>${prices[0]}</span>
    </div>
  )
}

// Usage
;<ProductCard
  details={{ name: 'Laptop', description: 'Fast computer' }}
  prices={[999, 1299, 1599]}
/>

Summary

  • Arrays passed inline use single curly braces: propName={[values]}
  • Objects passed inline use double curly braces: propName={{key: value}}
  • First {} is JSX expression, second {} is object literal
  • Inline styles require object syntax: style={{property: value}}
  • CSS properties must use camelCase (backgroundColor, not background-color)
  • Number values default to pixels, other units need strings
  • Prefer className for most styling, use inline styles for dynamic values

Destructuring Props

Overview

Destructuring is a JavaScript syntax for extracting values from objects. In React, it provides cleaner prop access in components.

Core Concepts

JavaScript Destructuring

Instead of accessing object properties individually:

const person = {
  firstName: 'Sam',
  lastName: 'Doe',
  age: 24,
}

const firstName = person.firstName
const lastName = person.lastName

Use destructuring for shorter syntax:

const { firstName, lastName } = person

Destructuring Props in Component Body

Before destructuring:

function WelcomeUser(props) {
  const username = props.username
  const notifications = props.notifications

  return (
    <div>
      Welcome {username}! You've got {notifications} unread notifications.
    </div>
  )
}

With destructuring:

function WelcomeUser(props) {
  const { username, notifications } = props

  return (
    <div>
      Welcome {username}! You've got {notifications} unread notifications.
    </div>
  )
}

Destructuring in Function Parameter

Destructure directly in the parameter:

function WelcomeUser({ username, notifications }) {
  return (
    <p>
      Welcome {username}! You've got {notifications} unread notifications.
    </p>
  )
}

This replaces props with {username, notifications}, automatically extracting props.username and props.notifications into variables.

When to use:

  • Few props (2-4): parameter destructuring is clean
  • Many props (5+): consider props object for readability

Usage

// Parameter destructuring - clean for few props
function UserCard({ name, email, avatar }) {
  return (
    <div>
      <img src={avatar} alt={name} />
      <h3>{name}</h3>
      <p>{email}</p>
    </div>
  )
}

// Body destructuring - better for many props
function ProductDetails(props) {
  const { name, price, description, rating, reviews, inStock } = props

  return (
    <div>
      <h2>{name}</h2>
      <p>{description}</p>
      <span>${price}</span>
      <div>
        Rating: {rating} ({reviews} reviews)
      </div>
      {inStock && <button>Add to Cart</button>}
    </div>
  )
}

Summary

  • Destructuring extracts object properties into variables: const {a, b} = obj
  • Props can be destructured in component body: const {username} = props
  • Props can be destructured in parameter: function Component({username})
  • Parameter destructuring is common in React codebases
  • Use body destructuring for many props to maintain readability
  • Both approaches are valid - choose based on code clarity

Destructuring with Default Values

Overview

JavaScript destructuring supports default values for missing properties. This feature enables React components to define fallback values for optional props.

Core Concepts

JavaScript Default Values

Provide default during destructuring:

const person = {
  firstName: 'Sam',
  lastName: 'Doe',
  age: 24,
}

const { firstName, lastName, role = 'user' } = person
// role is "user" because it doesn't exist on person

Default Prop Values

Without destructuring (ternary):

function WelcomeUser(props) {
  const username = props.username
  const notifications = props.notifications ? props.notifications : 0

  return (
    <div>
      Welcome {username}! You've got {notifications} unread notifications.
    </div>
  )
}

With body destructuring:

function WelcomeUser(props) {
  const { username, notifications = 0 } = props

  return (
    <div>
      Welcome {username}! You've got {notifications} unread notifications.
    </div>
  )
}

With parameter destructuring:

function WelcomeUser({ username, notifications = 0 }) {
  return (
    <div>
      Welcome {username}! You've got {notifications} unread notifications.
    </div>
  )
}

When Defaults Apply

Default values only apply when prop is:

  • Not provided: <WelcomeUser username="Sam" />
  • Explicitly undefined: <WelcomeUser username="Sam" notifications={undefined} />

Defaults do not apply for:

  • null: <WelcomeUser notifications={null} />notifications is null
  • 0: <WelcomeUser notifications={0} />notifications is 0
  • Empty string: <WelcomeUser notifications="" />notifications is ""

Usage

// Simple defaults
function Button({text = "Click me", variant = "primary"}) {
    return <button className={variant}>{text}</button>;
}

// Usage examples
<Button text="Submit" variant="success" />
<Button text="Cancel" />  // variant defaults to "primary"
<Button />  // Both default: text="Click me", variant="primary"

// Multiple defaults with complex values
function UserProfile({
    name,
    avatar = "/default-avatar.png",
    bio = "No bio provided",
    followers = 0,
    verified = false
}) {
    return (
        <div>
            <img src={avatar} alt={name} />
            <h2>{name} {verified && "✓"}</h2>
            <p>{bio}</p>
            <span>{followers} followers</span>
        </div>
    );
}

Summary

  • Destructuring supports default values: const {prop = defaultValue} = obj
  • Use = syntax during destructuring to provide defaults
  • Defaults apply when prop is missing or undefined
  • Defaults don't apply for null, 0, false, or ""
  • Works in both body destructuring and parameter destructuring
  • Parameter destructuring with many defaults can reduce readability
  • Useful for optional configuration props and UI customization

Summary

JSX Comments:

  • Must wrap comments with {/* comment */} syntax
  • Cannot use // directly in JSX (interpreted as string)
  • Curly braces break out from JSX into JavaScript world

JSX Blank Space Expression:

  • JSX trims blank spaces at beginning/end of lines
  • Use {" "} to preserve spaces between elements
  • Prettier formatter often adds this automatically
  • Without {" "}, text runs together: HelloWorld.

Props - Arrays and Objects:

  • Arrays passed inline: propName={[values]}
  • Objects passed inline: propName={{key: value}} (double braces)
  • First {} for JSX expression, second {} for object literal

Inline Styles:

  • Passed as object: style={{key: value}}
  • CSS properties use camelCase: backgroundColor, fontSize

Destructuring Props:

  • JavaScript feature for extracting values from objects
  • Can destructure directly in function parameters
  • Supports default values: function Component({name = "Guest"}) {}
  • Provides default values for missing props
  • Shorter syntax for accessing prop values

Click Events in React

Overview

React enables interactive components through event handlers. The onClick prop adds click event handling to JSX elements.

Core Concepts

Basic onClick Syntax

Add click handler to button:

function App() {
  return <button onClick={() => console.log('Button clicked')}>Buy now</button>
}

Syntax breakdown:

  1. onClick prop (capital C)
  2. Curly braces {} for JSX expression
  3. Arrow function definition: () => console.log("Button clicked")

Function Definition Required

✅ Correct - function definition:

<button onClick={() => console.log('Button clicked')}>Buy now</button>

❌ Incorrect - immediate execution:

<button onClick={console.log('Button clicked')}>Buy now</button>

The incorrect version logs immediately when component renders, not on click.

Why arrow function needed:

  • onClick expects a function reference
  • Without () =>, code executes during render
  • Arrow function wraps code, preventing immediate execution

Inline Event Handlers

For simple actions, use inline arrow functions:

function App() {
  return (
    <div>
      <button onClick={() => console.log('Add to cart')}>Add to Cart</button>
      <button onClick={() => alert('Checkout started')}>Checkout</button>
    </div>
  )
}

Accessibility Requirements

Only use onClick on interactive elements:

// ✅ Accessible - button is interactive element
<button onClick={() => handleClick()}>Click me</button>

// ❌ Inaccessible - paragraph isn't interactive
<p onClick={() => handleClick()}>Click me</p>

Why buttons only:

  • Screen readers announce buttons as clickable
  • Keyboard navigation works automatically (Enter/Space keys)
  • Semantic HTML improves accessibility
  • Non-button elements require extra ARIA attributes and keyboard handling

Usage

function ProductCard() {
  return (
    <div>
      <h2>Premium Headphones</h2>
      <p>$299.99</p>
      <button onClick={() => console.log('Added to cart')}>Add to Cart</button>
      <button onClick={() => console.log('Added to favorites')}>
        ♥ Favorite
      </button>
    </div>
  )
}

Summary

  • onClick prop adds click event handlers to JSX elements
  • Must use capital C: onClick not onclick
  • Pass function definition: onClick={() => code}
  • Missing () => causes immediate execution during render
  • Use inline arrow functions for simple one-line actions
  • Only use onClick on <button> elements for accessibility
  • Screen readers and keyboard users can't interact with non-button click handlers
  • Named handlers covered in next lesson for complex logic

Named Event Handlers

Overview

Named event handlers extract inline functions into dedicated functions, enabling multi-line logic and improved code organization.

Core Concepts

From Inline to Named Handler

Inline syntax (previous lesson):

function App() {
  return <button onClick={() => console.log('Button clicked')}>Buy now</button>
}

Named handler (two steps):

Step 1: Define the function

function App() {
  function handleClick() {
    console.log('Button clicked')
  }

  return <button>Buy now</button>
}

Step 2: Pass function reference

function App() {
  function handleClick() {
    console.log('Button clicked')
  }

  return <button onClick={handleClick}>Buy now</button>
}

Function Reference vs Function Call

✅ Correct - function reference:

<button onClick={handleClick}>Buy now</button>

❌ Incorrect - function call:

<button onClick={handleClick()}>Buy now</button>

The incorrect version executes immediately during render, breaking the event handler.

Multi-line Logic

Named handlers support complex logic:

function App() {
  function handleCheckout() {
    console.log('Starting checkout')
    validateCart()
    calculateTotal()
    redirectToPayment()
  }

  return <button onClick={handleCheckout}>Checkout</button>
}

Naming Conventions

handleEvent pattern:

function handleClick() {}
function handleChange() {}
function handleSubmit() {}

handleSubject pattern:

function handleLogin() {}
function handleAddToCart() {}
function handleLogout() {}

handleSubjectEvent pattern (for many events):

function handleButtonClick() {}
function handleFormChange() {}
function handleModalClose() {}

Usage

function ProductCard() {
  function handleAddToCart() {
    console.log('Adding to cart')
    updateCartCount()
    showNotification('Item added!')
  }

  function handleFavorite() {
    console.log('Adding to favorites')
    saveFavorite()
    updateIcon()
  }

  return (
    <div>
      <h2>Premium Headphones</h2>
      <button onClick={handleAddToCart}>Add to Cart</button>
      <button onClick={handleFavorite}>♥ Favorite</button>
    </div>
  )
}

// Inline syntax acceptable for simple one-liners
function SimpleButton() {
  return <button onClick={() => console.log('Clicked')}>Click</button>
}

Summary

  • Named event handlers are functions defined inside components
  • Extract inline arrow functions into named functions for multi-line logic
  • Pass function reference: onClick={handleClick} not onClick={handleClick()}
  • Adding () calls function immediately during render
  • Use handleEvent, handleSubject, or handleSubjectEvent naming patterns
  • Inline syntax acceptable for simple one-line handlers
  • Named handlers are preferred for complex logic and reusability

Summary

Event Handling in React:

  • Click events: onClick prop with function definition
  • Use onClick only on button elements (semantics/accessibility)
  • Inline syntax: onClick={() => console.log("clicked")} (don't forget () =>)
  • Without () =>, code executes during render (wrong)

Named Event Handlers:

  • Function defined inside component to handle events
  • Supports multiple lines of code (preferred syntax)
  • Naming conventions: handleEvent, handleSubject, handleSubjectEvent
  • Pass function reference, not call: onClick={handleClick} not onClick={handleClick()}

Other Event Props:

  • onChange - for input, textarea, select elements
  • onInput - for input, textarea, select elements
  • onKeyDown, onKeyUp, onKeyPress - keyboard events
  • onSubmit - for form elements
  • Event names follow camelCase: onKeyDown not onkeydown

Introduction to React Hooks

Overview

Hooks enable function components to access React internals like rendering, state management, and lifecycle events. They replaced the older class-based component system.

Core Concepts

React Evolution

Class components (deprecated):

  • Old approach with complicated gotchas
  • React moved away from classes for components
  • Classes still useful in JavaScript (e.g., lit library)
  • React's class implementation was convoluted

Function components (current):

  • What you've been learning throughout this course
  • Simpler, more predictable syntax
  • Require hooks to access React internals

What Hooks Enable

Hooks let you "hook into" React internals from function components:

1. State management (useState):

  • Create and update component-private data
  • Updating state automatically re-renders component
  • Most commonly used hook

2. DOM references (useRef):

  • Get reference to specific DOM elements
  • Programmatically focus inputs
  • Access DOM properties directly

3. Side effects (useEffect):

  • Run code after component renders
  • Log analytics, fetch data, set timers
  • Considered "escape hatch" - use sparingly

Why "Hooks"?

Function components are just functions that return JSX:

function App() {
  return <h1>Hello</h1>
}

Without additional features, they can't:

  • Store data that persists between renders
  • Perform actions after rendering
  • Access DOM elements directly

Hooks provide these powerful capabilities by hooking into React's internal render process.

Hook Naming Convention

All hooks start with use:

  • useState - manage state
  • useEffect - run side effects
  • useRef - reference DOM elements
  • useId - generate unique IDs
  • useContext - access context
  • useMemo - memoize values
  • useCallback - memoize functions

The use prefix is required - it's how React identifies hooks.

Learning Path

Start with simplest hook: We'll begin with useId (simplest hook) to understand:

  • How to import and call hooks
  • Where hooks go in components
  • Basic hook syntax and rules

Then learn common hooks:

  • useState (most important - component state)
  • useEffect (side effects and lifecycle)
  • Others as needed

This gradual approach prevents overwhelming you with multiple concepts simultaneously.

Summary

  • React evolved from class components to function components
  • Hooks enable function components to access React internals
  • useState manages state (most common hook)
  • useRef accesses DOM elements
  • useEffect runs code after render (escape hatch)
  • All hooks start with use prefix
  • We'll start learning with useId (simplest hook)
  • Function components + hooks = powerful, clean React code

The useId Hook

Overview

useId generates unique, consistent IDs for accessibility in forms. It solves the ID duplication problem when reusing components.

Core Concepts

Accessible Forms Require Labels

❌ Inaccessible - no label:

<form>
  <input type="text" placeholder="Username" id="username" />
  <input type="submit" value="Go" />
</form>

Screen readers can't identify input purpose from placeholder alone.

✅ Accessible - with label:

<form>
  <label for="username">Enter your username:</label>
  <input type="text" placeholder="Username" id="username" />
  <input type="submit" value="Go" />
</form>

The for attribute links label to input via id.

JSX: htmlFor Instead of for

JavaScript reserves for keyword, so JSX uses htmlFor:

function UserForm() {
  return (
    <form>
      <label htmlFor="username">Enter your username:</label>
      <input type="text" placeholder="Username" id="username" />
      <input type="submit" value="Go" />
    </form>
  )
}

Console warning if wrong:

Invalid DOM property for. Did you mean htmlFor?

Component Reuse Problem

Hardcoded IDs break when component used multiple times:

function App() {
  return (
    <>
      <UserForm /> {/* id="username" */}
      <UserForm /> {/* id="username" - DUPLICATE! */}
    </>
  )
}

Duplicate IDs break accessibility and violate HTML spec.

useId Solution

useId generates unique ID for each component instance:

Step 1: Import from React

import { useId } from 'react'

All React hooks are named exports.

Step 2: Call at component top

import { useId } from 'react'

function UserForm() {
  const id = useId() // Must be called at top of component
  console.log(id) // Logs: «r0» or _r0_

  return (
    <form>
      <label htmlFor="username">Enter your username:</label>
      <input type="text" placeholder="Username" id="username" />
      <input type="submit" value="Go" />
    </form>
  )
}

Step 3: Use generated ID

import { useId } from 'react'

function UserForm() {
  const id = useId()

  return (
    <form>
      <label htmlFor={id}>Enter your username:</label>
      <input type="text" placeholder="Username" id={id} />
      <input type="submit" value="Go" />
    </form>
  )
}

Now each <UserForm /> instance gets unique ID: «r0», «r1», «r2», etc.

How It Works

useId hooks into React's rendering process:

  • Generates unique ID per component instance
  • Same ID across re-renders (consistent)
  • Different ID for different instances (unique)
  • Enables safe component reuse

Usage

import { useId } from 'react'

function LoginForm() {
  const usernameId = useId()
  const passwordId = useId()

  return (
    <form>
      <div>
        <label htmlFor={usernameId}>Username:</label>
        <input type="text" id={usernameId} />
      </div>
      <div>
        <label htmlFor={passwordId}>Password:</label>
        <input type="password" id={passwordId} />
      </div>
      <button type="submit">Login</button>
    </form>
  )
}

// Can now reuse safely
function App() {
  return (
    <>
      <LoginForm /> {/* Unique IDs */}
      <LoginForm /> {/* Different unique IDs */}
    </>
  )
}

Summary

  • Form inputs require labels linked via for attribute for accessibility
  • JSX uses htmlFor instead of for (reserved keyword)
  • Hardcoded IDs break when component reused
  • useId generates unique, consistent IDs per component instance
  • Import: import {useId} from "react"
  • Call at top of component: const id = useId()
  • Use in both label and input: htmlFor={id} and id={id}
  • Each instance gets unique ID: «r0», «r1», etc.
  • Hooks into React rendering process (explained next lesson)

Rules of Hooks

Overview

React hooks require specific placement and ordering to function correctly. Two strict rules govern hook usage.

Core Concepts

Render Tree

React tracks components in a render tree structure:

function App() {
  return (
    <>
      <Sidebar />
      <Content />
      <Footer />
    </>
  )
}

Render tree visualization:

<App />
├── <Sidebar />
├── <Content />
└── <Footer />

When you call a hook, React tracks its position in this tree:

function Content() {
  const id = useId() // React tracks: "useId in Content component"
  return <p>Lorem ipsum...</p>
}

Even if you use <Content /> multiple times, React distinguishes between instances because it knows the exact position in the render tree.

Stable Order Requirement

React reconstructs the render tree on every render. For this to work:

  • Same hooks must be called every time
  • In same order every time
  • Same number of hooks every time

❌ Breaks stable order:

function App(props) {
  if (props.admin) {
    const adminId = useId() // Sometimes called, sometimes not
  }
}

This conditional hook breaks React's internal tracking.

✅ Maintains stable order:

function App(props) {
  const adminId = useId() // Always called
  if (props.admin) {
    console.log(adminId) // Logic can be conditional
  }
}

Hook must always be called, but using the value can be conditional.

The Rules of Hooks

Rule #1: Only Call Hooks from React Functions

Hooks only work inside:

  • React components (functions starting with capital letter)
  • Custom hooks (covered later)
import { useId } from 'react'

function App() {
  // ✅ Hook inside React component
  const id = useId()
}

function emailUser() {
  // ❌ Hook inside regular function - ERROR
  const id = useId()
}

Why: Hooks belong to React's render tree. Regular functions aren't part of this tree.

Rule #2: Only Call Hooks at Top Level

Never call hooks inside:

  • Conditionals (if, switch, ternary)
  • Loops (for, while, forEach)
  • Nested functions
import { useId } from 'react'

function App(props) {
  // ❌ Hook inside if statement
  if (props.admin) {
    const adminId = useId()
  }

  // ❌ Hook inside loop
  for (let i = 0; i < 5; i++) {
    const id = useId()
  }

  // ❌ Hook inside nested function
  function helper() {
    const id = useId()
  }
}

Fix: Move hooks to top of component:

function App(props) {
  // ✅ All hooks at top level
  const adminId = useId()
  const userId = useId()

  // Conditional logic can use hook values
  if (props.admin) {
    console.log(adminId)
  }

  return /* JSX */
}

Standard Component Structure

Most React components follow this pattern:

function ComponentName() {
  // 1. Hooks calls at top
  const id = useId()
  const [state, setState] = useState()

  // 2. Other logic, conditions, functions
  function handleClick() {}

  // 3. Return JSX
  return /* ... */
}

Violation Error

Breaking rules shows this error:

Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:

  1. You might have mismatching versions of React and the renderer (such as React DOM)
  2. You might be breaking the Rules of Hooks
  3. You might have more than one copy of React in the same app.

Usage

✅ Correct hook usage:

import { useId } from 'react'

function LoginForm({ showPassword }) {
  // All hooks at top, always called
  const usernameId = useId()
  const passwordId = useId()
  const emailId = useId()

  // Conditional logic uses hook values
  const inputId = showPassword ? passwordId : emailId

  return (
    <form>
      <label htmlFor={usernameId}>Username</label>
      <input id={usernameId} />

      <label htmlFor={inputId}>{showPassword ? 'Password' : 'Email'}</label>
      <input id={inputId} />
    </form>
  )
}

❌ Incorrect - conditional hooks:

function LoginForm({ showPassword }) {
  const usernameId = useId()

  // ❌ Conditional hook call
  if (showPassword) {
    const passwordId = useId()
  }

  return /* JSX */
}

Summary

  • Render tree represents component organization
  • React tracks hooks by position in render tree
  • Hooks require stable order (same hooks, same order, every render)
  • Rule #1: Only call hooks from React components (or custom hooks)
  • Rule #2: Only call hooks at top level (no conditions, loops, nested functions)
  • Always call all hooks, even if you don't use the value
  • Conditional logic can use hook values, but hook calls must be unconditional
  • Standard pattern: hooks at top → logic in middle → return JSX at bottom
  • Breaking rules triggers "Invalid hook call" error
  • More complex examples with useState in later chapters

Summary

React Evolution:

  • Moved from class components to function components
  • Hooks introduced to access React internals in function components
  • Hooks add powerful features to function components
  • All hooks start with use prefix

The useId Hook:

  • Generates consistent, unique ID per component
  • ID stays same across renders
  • Returns new unique ID for every new component instance
  • Must be called inside component at top level

Forms and Labels:

  • Each input/select needs related label element
  • for attribute in HTML becomes htmlFor in JSX
  • Important for accessibility

React Render Tree:

  • Structure representing component organization
  • React tracks hooks by position in render tree
  • Hooks expect stable order

Rules of Hooks:

  1. Only call hooks from React functions
  2. Only call hooks at top level of component
  • Cannot call inside conditions, loops, or nested functions
  • Must maintain same order on every render

Array Destructuring Prerequisite

Overview

Array destructuring is JavaScript syntax for extracting values from arrays into variables. It's essential for understanding useState hook syntax.

Core Concepts

Basic Array Destructuring

Without destructuring:

const point = [10.5, 21.3]

const lat = point[0]
const lng = point[1]

With destructuring:

const point = [10.5, 21.3]

const [lat, lng] = point

Both approaches are equivalent. Destructuring syntax [lat, lng] assigns:

  • First array item → lat variable
  • Second array item → lng variable

Destructuring Function Returns

Without destructuring:

function getPoint() {
  return [10.5, 21.3]
}

const point = getPoint()
const lat = point[0]
const lng = point[1]

With destructuring (single line):

function getPoint() {
  return [10.5, 21.3]
}

const [lat, lng] = getPoint()

Assumes getPoint() always returns array with two elements.

Mixed Data Types

Arrays can contain different types (number, string, function, boolean):

Number and function example:

Without destructuring:

function getUserName() {
  return 'Sam Green'
}

function getUser() {
  return [15, getUserName] // [number, function]
}

const user = getUser()
const id = user[0] // 15
const getName = user[1] // function reference
console.log(id) // 15
console.log(getName()) // "Sam Green" - must call function

With destructuring:

function getUserName() {
  return 'Sam Green'
}

function getUser() {
  return [15, getUserName]
}

const [id, getName] = getUser()
console.log(id) // 15
console.log(getName()) // "Sam Green"

Important: getName is a function, so call it with () to get value.

Why This Matters for useState

The useState hook returns [value, setterFunction]:

// useState pattern (preview)
const [count, setCount] = useState(0)
//     ↑       ↑
//   number  function

Understanding array destructuring with mixed types prepares you for useState syntax.

Usage Examples

String and boolean:

function getStatus() {
  return ['active', true]
}

const [status, isOnline] = getStatus()
console.log(status) // "active"
console.log(isOnline) // true

Number and string:

function getProduct() {
  return [99.99, 'Headphones']
}

const [price, name] = getProduct()
console.log(price) // 99.99
console.log(name) // "Headphones"

Multiple functions:

function getHandlers() {
  return [() => console.log('Save'), () => console.log('Cancel')]
}

const [handleSave, handleCancel] = getHandlers()
handleSave() // "Save"
handleCancel() // "Cancel"

Summary

  • Array destructuring extracts array items into variables: const [a, b] = array
  • Shorter syntax than accessing via index: array[0], array[1]
  • Works with function return values: const [x, y] = getCoords()
  • Arrays can contain mixed types: numbers, strings, functions, booleans
  • When destructuring functions, remember to call them: getName()
  • Common pattern: [value, function] - important for useState hook
  • Destructuring doesn't modify original array
  • Position matters: first variable gets first item, second gets second item
  • Prepares you for useState syntax: const [state, setState] = useState()

The useState Hook

Overview

useState creates component state - internal memory that can change over time and trigger UI updates automatically. It's the most commonly used React hook.

Core Concepts

What is State?

State is component-internal data that:

  • Changes over time (e.g., counter value, current song index)
  • Triggers automatic UI updates when changed
  • Remains independent per component instance
  • Makes components interactive and dynamic

Examples:

  • Counter value that increments on click
  • Current song playing in playlist
  • Form input values
  • Toggle switch on/off state

useState Syntax

Import:

import { useState } from 'react'

Basic usage:

import { useState } from 'react'

function Counter() {
  const [counter, setCounter] = useState(0)

  return <p>The counter is {counter}</p>
}

What happens:

  1. useState(0) called with initial value 0
  2. Returns array: [currentValue, updaterFunction]
  3. Array destructuring: [counter, setCounter]
  4. counter is the state variable (starts at 0)
  5. setCounter is function to update state

Array Destructuring

Without destructuring (not recommended):

function Counter() {
  const state = useState(0)
  const counter = state[0] // state value
  const setCounter = state[1] // setter function

  return <p>The counter is {counter}</p>
}

With destructuring (always use this):

function Counter() {
  const [counter, setCounter] = useState(0)

  return <p>The counter is {counter}</p>
}

Initial Value

Provide initial value as argument:

const [counter, setCounter] = useState(0) // starts at 0
const [index, setIndex] = useState(0) // starts at 0
const [name, setName] = useState('') // starts at ""
const [isOpen, setIsOpen] = useState(false) // starts at false

If no initial value provided, defaults to undefined:

const [data, setData] = useState() // starts as undefined

Naming Convention

Pattern: [stateName, setStateName]

// ✅ Correct naming
const [counter, setCounter] = useState(0)
const [index, setIndex] = useState(0)
const [name, setName] = useState('')
const [isOpen, setIsOpen] = useState(false)
const [todos, setTodos] = useState([])

// ❌ Incorrect naming
const [counter, updateCounter] = useState(0) // should be setCounter
const [index, changeIndex] = useState(0) // should be setIndex

Always use set + state variable name in camelCase.

Rules of Hooks Apply

useState follows hooks rules:

  • Must be called at top level (not in conditions/loops)
  • Must be called in React components only
  • Must be called in same order every render
function Counter() {
  // ✅ Correct - top level
  const [counter, setCounter] = useState(0)

  // ❌ Wrong - inside condition
  if (someCondition) {
    const [value, setValue] = useState(0)
  }

  return <p>{counter}</p>
}

Usage

import { useState } from 'react'

function Playlist() {
  const [currentIndex, setCurrentIndex] = useState(0)
  const [volume, setVolume] = useState(50)
  const [isPlaying, setIsPlaying] = useState(false)

  const songs = ['Song A', 'Song B', 'Song C']

  return (
    <div>
      <p>Now playing: {songs[currentIndex]}</p>
      <p>Volume: {volume}%</p>
      <p>Status: {isPlaying ? 'Playing' : 'Paused'}</p>
    </div>
  )
}

Summary

  • State is component's internal memory that changes over time
  • State changes trigger automatic, efficient UI updates
  • useState creates state variable: const [state, setState] = useState(initial)
  • Returns array: [currentValue, setterFunction]
  • Always use array destructuring
  • Naming: [stateName, setStateName]
  • Initial value used only on first render
  • Defaults to undefined if no initial value
  • Must follow Rules of Hooks (top level, React components only)
  • Each component instance has independent state
  • Next lesson covers updating state with setState function

Updating State in React

Overview

State updates in React must use the setState function. Direct variable modification doesn't trigger re-renders. Calling setState hooks into React's render cycle, updating the UI automatically.

Core Concepts

Wrong: Direct Modification

// ❌ Doesn't work - React won't know state changed
function Counter() {
  const [counter, setCounter] = useState(0)

  function handleClick() {
    counter = counter + 1 // ❌ Direct modification
  }

  return <button onClick={handleClick}>{counter}</button>
}

React won't detect this change and component won't re-render.

Correct: setState Function

// ✅ Works - triggers re-render
function Counter() {
  const [counter, setCounter] = useState(0)

  function handleClick() {
    setCounter(counter + 1) // ✅ Use setState
  }

  return <button onClick={handleClick}>{counter}</button>
}

How Re-rendering Works

First render:

  1. Component called, useState(0) returns [0, setCounter]
  2. counter is 0
  3. JSX rendered: <p>The counter is 0</p>

User clicks button:

  1. handleClick called
  2. setCounter(counter + 1)setCounter(0 + 1)setCounter(1)
  3. React detects state change

Second render:

  1. Component called again
  2. useState(0) returns [1, setCounter] (initial value ignored)
  3. counter is now 1
  4. JSX rendered: <p>The counter is 1</p>
  5. React compares new JSX to previous
  6. React efficiently updates only what changed in DOM

Third render (user clicks again):

  1. setCounter(1 + 1)setCounter(2)
  2. Component re-renders with counter = 2
  3. DOM updated efficiently

Complete Working Example

import { useState } from 'react'

function Counter() {
  const [counter, setCounter] = useState(0)

  function handleClick() {
    setCounter(counter + 1)
  }

  return (
    <>
      <p>The counter is {counter}</p>
      <button onClick={handleClick}>Add 1</button>
    </>
  )
}

Flow:

  • Initial: counter = 0, displays "The counter is 0"
  • Click: calls setCounter(1), re-renders with counter = 1
  • Click: calls setCounter(2), re-renders with counter = 2
  • And so on...

React's Reconciliation

React doesn't re-render entire page on state change:

  1. Component re-runs, generates new JSX
  2. React compares new JSX to previous (virtual DOM diffing)
  3. Only changed elements updated in actual DOM
  4. Extremely efficient process

This is React DOM's reconciliation algorithm.

Important Rules

Only call setState inside events (for now):

function Counter() {
  const [counter, setCounter] = useState(0)

  // ✅ Good - inside event handler
  function handleClick() {
    setCounter(counter + 1)
  }

  // ❌ Bad - outside event (causes infinite loop)
  setCounter(counter + 1)

  return <button onClick={handleClick}>{counter}</button>
}

Calling setState at component top level creates infinite loop:

  • Component renders
  • setState called
  • Triggers re-render
  • Component renders again
  • setState called again
  • Infinite loop

Usage

import { useState } from 'react'

function ShoppingCart() {
  const [itemCount, setItemCount] = useState(0)

  function handleAdd() {
    setItemCount(itemCount + 1)
  }

  function handleRemove() {
    setItemCount(itemCount - 1)
  }

  function handleReset() {
    setItemCount(0)
  }

  return (
    <div>
      <p>Items in cart: {itemCount}</p>
      <button onClick={handleAdd}>Add Item</button>
      <button onClick={handleRemove}>Remove Item</button>
      <button onClick={handleReset}>Clear Cart</button>
    </div>
  )
}

Summary

  • Must use setState function to update state, never direct modification
  • Direct modification doesn't trigger re-renders
  • setState hooks into React internals, triggers component re-render
  • On re-render, useState returns updated value (ignores initial value)
  • React compares new JSX to previous, updates only what changed
  • This reconciliation process is extremely efficient
  • Only call setState inside event handlers (for now)
  • Calling setState at top level creates infinite loop
  • Next lesson: conditional state updates

Summary

Array Destructuring:

  • Assign multiple variables at once from array
  • Works with functions returning arrays
  • Array items can be different data types

useState Basics:

  • Creates component's internal memory (state)
  • Returns array: [stateValue, setStateFunction]
  • Always use array destructuring: const [value, setValue] = useState(initialValue)
  • Naming convention: stateName and setStateName in camelCase
  • Must call at top level, only in React components
  • Accepts initial value for first render

State Updates:

  • Use setState function to update state (never modify directly)
  • Calling setState triggers React to re-render component
  • React compares new JSX to previous, efficiently updates changed DOM elements

How State Works:

  • State is component's memory that changes over time
  • State changes automatically update UI
  • State variable holds current value
  • Updater function hooks into React internals
  • Re-render shows new state value in UI

Conditional State Updates

Overview

While useState must always be called at top level, the setState function can be called conditionally inside event handlers. This enables controlled state updates based on conditions.

Core Concepts

The Problem

Countdown going below zero:

import { useState } from 'react'

function Countdown() {
  const [times, setTimes] = useState(10)

  function handleClick() {
    setTimes(times - 1) // Goes negative: 10, 9, 8... 0, -1, -2
  }

  return (
    <>
      <p>{times} times remaining</p>
      <button onClick={handleClick}>Count down</button>
    </>
  )
}

Wrong Solution: Conditional useState

// ❌ NEVER do this - violates Rules of Hooks
function Countdown() {
  if (someCondition) {
    const [times, setTimes] = useState(10) // ❌ Conditional hook
  }

  // ...
}

Remember: useState must always be called at top level, never conditionally.

Correct Solution: Conditional setState

import { useState } from 'react'

function Countdown() {
  const [times, setTimes] = useState(10) // ✅ Always called at top

  function handleClick() {
    // ✅ setState can be conditional
    if (times > 0) {
      setTimes(times - 1)
    }
  }

  return (
    <>
      <p>{times} times remaining</p>
      <button onClick={handleClick}>Count down</button>
    </>
  )
}

Result: Counter stops at 0, never goes negative.

Rule Summary

Can Be Conditional?LocationExample
❌ NOuseState() hook callNever wrap with if
✅ YESsetState() function callCan wrap with if inside handlers

Alternative Implementations

All valid approaches to prevent negative countdown:

Guard clause:

function handleClick() {
  if (times === 0) return
  setTimes(times - 1)
}

Maximum of 0:

function handleClick() {
  setTimes(Math.max(0, times - 1))
}

Ternary operator:

function handleClick() {
  setTimes(times > 0 ? times - 1 : 0)
}

All accomplish same result - choose based on readability.

Usage Examples

Prevent exceeding maximum:

function Counter() {
  const [count, setCount] = useState(0)
  const MAX = 100

  function handleIncrement() {
    if (count < MAX) {
      setCount(count + 1)
    }
  }

  return <button onClick={handleIncrement}>Count: {count}</button>
}

Range-bounded value:

function Volume() {
  const [volume, setVolume] = useState(50)

  function handleIncrease() {
    if (volume < 100) {
      setVolume(volume + 10)
    }
  }

  function handleDecrease() {
    if (volume > 0) {
      setVolume(volume - 10)
    }
  }

  return (
    <div>
      <p>Volume: {volume}%</p>
      <button onClick={handleDecrease}>-</button>
      <button onClick={handleIncrease}>+</button>
    </div>
  )
}

Conditional based on other state:

function Game() {
  const [score, setScore] = useState(0)
  const [gameOver, setGameOver] = useState(false)

  function handleScore() {
    // Only update if game not over
    if (!gameOver) {
      setScore(score + 10)
    }
  }

  return <button onClick={handleScore}>Score: {score}</button>
}

Summary

  • useState() hook cannot be conditional (violates Rules of Hooks)
  • useState() must always be called at component top level
  • setState() function can be conditional inside event handlers
  • Use if conditions to control when state updates
  • Enables bounded values (minimum, maximum, ranges)
  • Multiple valid approaches: if, guard clause, Math.max/min, ternary
  • Choose approach based on code readability
  • Common pattern: prevent negative values, enforce maximums

Closures and Event Handler Parameters

Overview

Closures enable inner functions to access outer function variables. This JavaScript feature allows event handlers to access state variables and component data. Event handlers can accept parameters using arrow function wrappers.

Core Concepts

What Are Closures?

Closure: Inner function accessing outer function's variables, even after outer function completes.

import { useState } from 'react'

function Counter() {
  const [counter, setCounter] = useState(0)

  function handleClick() {
    // handleClick has access to `counter` and `setCounter`
    // This is a closure
    setCounter(counter + 1)
  }

  return <button onClick={handleClick}>Add 1</button>
}

How it works:

  • handleClick defined inside Counter function
  • handleClick can access Counter's variables (counter, setCounter)
  • Access persists even when handleClick called later (on click)

You don't "enable" closures - they happen automatically with nested functions.

Event Handlers with Parameters

Problem: Multiple buttons need different values:

function Counter() {
  const [counter, setCounter] = useState(0)

  return (
    <>
      <p>The counter is {counter}</p>
      <button>Add 1</button>
      <button>Add 5</button>
    </>
  )
}

Solution: Parameterized handler function:

Step 1: Make handler accept parameter

function handleIncrement(value) {
  setCounter(counter + value)
}

Step 2: Wrap with arrow function

<button onClick={() => handleIncrement(1)}>Add 1</button>
<button onClick={() => handleIncrement(5)}>Add 5</button>

Complete Example

import { useState } from 'react'

function Counter() {
  const [counter, setCounter] = useState(0)

  function handleIncrement(value) {
    setCounter(counter + value)
  }

  return (
    <>
      <p>The counter is {counter}</p>
      <button onClick={() => handleIncrement(1)}>Add 1</button>
      <button onClick={() => handleIncrement(5)}>Add 5</button>
    </>
  )
}

Common Mistake: Immediate Execution

❌ Incorrect - calls function immediately:

<button onClick={handleIncrement(1)}>Add 1</button>
<button onClick={handleIncrement(5)}>Add 5</button>

This executes during render, not on click.

✅ Correct - wraps in arrow function:

<button onClick={() => handleIncrement(1)}>Add 1</button>
<button onClick={() => handleIncrement(5)}>Add 5</button>

Arrow function creates function definition that React calls on click.

Default Parameters

Can use JavaScript default parameters:

function handleIncrement(value = 1) {
    setCounter(counter + value);
}

// Both work:
<button onClick={() => handleIncrement()}>Add 1</button>
<button onClick={() => handleIncrement(5)}>Add 5</button>

Usage Examples

Shopping cart quantity:

function CartItem() {
  const [quantity, setQuantity] = useState(1)

  function handleChangeQuantity(amount) {
    setQuantity(quantity + amount)
  }

  return (
    <div>
      <button onClick={() => handleChangeQuantity(-1)}>-</button>
      <span>{quantity}</span>
      <button onClick={() => handleChangeQuantity(1)}>+</button>
    </div>
  )
}

Multiple preset values:

function Timer() {
  const [seconds, setSeconds] = useState(0)

  function handleSetTimer(time) {
    setSeconds(time)
  }

  return (
    <div>
      <p>Timer: {seconds}s</p>
      <button onClick={() => handleSetTimer(30)}>30s</button>
      <button onClick={() => handleSetTimer(60)}>1m</button>
      <button onClick={() => handleSetTimer(300)}>5m</button>
    </div>
  )
}

Dynamic data from array:

function Playlist() {
  const [currentIndex, setCurrentIndex] = useState(0)
  const songs = ['Song A', 'Song B', 'Song C']

  function handleSelectSong(index) {
    setCurrentIndex(index)
  }

  return (
    <div>
      <p>Now playing: {songs[currentIndex]}</p>
      {songs.map((song, index) => (
        <button key={index} onClick={() => handleSelectSong(index)}>
          {song}
        </button>
      ))}
    </div>
  )
}

Summary

  • Closure: inner function accessing outer function variables automatically
  • Event handlers use closures to access state and component data
  • Closures happen automatically, not something you enable
  • Common React interview topic
  • Pass parameters to handlers by wrapping in arrow function: onClick={() => handler(param)}
  • Arrow wrapper prevents immediate execution during render
  • Without wrapper, function executes immediately (wrong)
  • Can use default parameters: function handler(value = 1) { }
  • Pattern: onClick={() => functionName(argument)}

Summary

Conditional State Changes:

  • Common use case in React applications
  • Cannot wrap useState hook with if condition (violates rules of hooks)
  • Can wrap setState function with if condition

Closures:

  • Inner function accesses outer function's variables
  • Persists even after outer function completes
  • Not something you choose - happens automatically when defining functions
  • Important for event handlers accessing component scope

Event Handler Parameters:

  • Pass parameters by wrapping in arrow function
  • Pattern: onClick={() => handleClick(param)}
  • Prevents immediate execution during render

React Rules: Props Immutability

Overview

React has strict rules for optimal performance and predictability. Props must be immutable (unchangeable) - components cannot modify props they receive. State is used for changing values.

Core Concepts

What is Immutability?

Immutable = unchangeable after creation.

React expects props to be immutable because:

  • Components should return same output for same props
  • Enables performance optimizations
  • Makes behavior predictable
  • Allows concurrent rendering

Main Rules of React

  1. Rules of Hooks (covered in chapter 17)
  2. State is immutable (5 dedicated chapters)
  3. Props are immutable (this lesson)
  4. Components must be pure (next lesson)

Props Cannot Be Modified

❌ Incorrect - modifying props:

function Counter(props) {
  function handleIncrement() {
    // ❌ Never modify props
    props.count += 1 // ERROR!
  }

  return (
    <div>
      <p>Count: {props.count}</p>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  )
}

Error thrown:

Uncaught TypeError: Cannot assign to read only property 'count' of object '#<Object>'

Why it fails:

  1. React throws error when modifying props
  2. Even if no error, React won't re-render
  3. ESLint plugin warns about prop modifications

Use State for Changing Values

✅ Correct - using state:

import { useState } from 'react'

function Counter(props) {
  const [count, setCount] = useState(0) // ✅ Use state, not props

  function handleIncrement() {
    setCount(count + 1) // ✅ Update state with setState
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  )
}

Props vs State

FeaturePropsState
Mutable❌ No✅ Yes
Triggers re-render❌ No✅ Yes
Owned byParentComponent
Updated byParentsetState
Use forPassing data downTracking changes

Usage Examples

Using prop as initial state:

function Counter({ initialCount }) {
  // ✅ Prop used only for initialization
  const [count, setCount] = useState(initialCount)

  function handleIncrement() {
    setCount(count + 1)
  }

  return <button onClick={handleIncrement}>Count: {count}</button>
}

// Usage
;<Counter initialCount={10} />

Reading props, using state for changes:

function UserProfile({ name, avatarUrl }) {
  const [isFollowing, setIsFollowing] = useState(false)

  function handleToggleFollow() {
    setIsFollowing(!isFollowing)
  }

  return (
    <div>
      <img src={avatarUrl} alt={name} />
      <h2>{name}</h2> {/* ✅ Props for display */}
      <button onClick={handleToggleFollow}>
        {isFollowing ? 'Unfollow' : 'Follow'} {/* ✅ State for interaction */}
      </button>
    </div>
  )
}

Derived values from props (computed, not mutated):

function ProductCard({ price, discount }) {
  // ✅ Compute new values, don't modify props
  const finalPrice = price - price * discount

  return (
    <div>
      <p>Original: ${price}</p>
      <p>Final: ${finalPrice}</p>
    </div>
  )
}

Summary

  • Props are immutable - cannot be modified after received
  • React throws error when attempting to modify props
  • Immutability enables performance optimizations and predictability
  • Components must return same output for same props (referential transparency)
  • Use state for values that need to change
  • Props passed from parent, state owned by component
  • Modifying props doesn't trigger re-render (even if no error)
  • ESLint plugin warns about prop modifications
  • Pattern: props for input data, state for interactive changes
  • Can compute derived values from props without mutation

React Rules: Pure Components

Overview

React requires components to be pure functions - functions that return the same output for the same inputs. This enables concurrent rendering and performance optimizations.

Core Concepts

What Are Pure Functions?

Pure function: Given same inputs, always produces same output.

✅ Pure function example:

function sum(a, b) {
  return a + b
}

sum(1, 4) // Always returns 5
sum(2, 6) // Always returns 8

❌ Impure function example:

let total = 0

function sum(nb) {
  total = total + nb // Modifies external variable
  return total
}

sum(5) // Returns 5
sum(5) // Returns 10 - different output for same input!

Pure Components

React components are functions, so same concept applies.

✅ Pure React component:

function WelcomeUser(props) {
  return <h1>Welcome {props.name}</h1>
}

// Same props always produces same JSX
;<WelcomeUser name="Sam" /> // Always renders "Welcome Sam"

❌ Impure React component:

let counter = 0

function Counter() {
  // ❌ Modifies external variable during render
  counter += 1

  return <h1>Counter: {counter}</h1>
}

// Same props produces different output each render

Why Purity Matters

Concurrent Rendering (React 18+):

  • React renders components in parallel
  • Can pause and resume rendering
  • Can skip re-rendering when inputs unchanged
  • Only possible with pure functions

Benefits:

  1. Performance: React can skip unchanged components
  2. Predictability: Same inputs = same output
  3. Easier testing: No hidden dependencies
  4. Concurrency-safe: Parallel rendering works correctly

StrictMode Detects Impurity

React's <StrictMode /> helps catch impure components:

How it works:

  1. First render (mount component)
  2. Unmount component
  3. Second render (mount again)

Purpose: Expose impure functions through double-rendering.

Development only: Production renders once (no performance impact).

Explains double console.logs:

function App() {
  console.log('Rendering') // Logs twice in dev
  return <h1>Hello</h1>
}

Impure Example with StrictMode

let renderCount = 0

function Counter() {
  // ❌ Impure - modifies external variable
  renderCount += 1
  console.log(`Render count: ${renderCount}`)

  return <h1>Counter: {renderCount}</h1>
}

// In StrictMode (dev):
// First render: "Render count: 1"
// Unmount
// Second render: "Render count: 2"
// Visible on page: "Counter: 2"
// Bug detected!

Correct Pattern: Use State

import { useState } from 'react'

function Counter() {
  // ✅ Pure - uses state instead of external variable
  const [count, setCount] = useState(0)

  function handleClick() {
    setCount(count + 1)
  }

  return (
    <>
      <h1>Counter: {count}</h1>
      <button onClick={handleClick}>Increment</button>
    </>
  )
}

Usage Examples

Pure component with derived values:

function ProductCard({ price, taxRate }) {
  // ✅ Pure - computation from props, no external mutations
  const total = price + price * taxRate
  const formattedPrice = `$${total.toFixed(2)}`

  return (
    <div>
      <p>Price: ${price}</p>
      <p>Total: {formattedPrice}</p>
    </div>
  )
}

Multiple instances with independent state:

function Counter() {
  const [count, setCount] = useState(0)

  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}

// Each instance maintains independent state
function App() {
  return (
    <>
      <Counter /> {/* Independent counter */}
      <Counter /> {/* Independent counter */}
    </>
  )
}

Summary

  • Pure functions return same output for same inputs
  • React components must be pure functions
  • Impure functions modify external state during rendering
  • Purity enables concurrent rendering (React 18+)
  • Benefits: performance, predictability, easier testing
  • Use state for values that change, not external variables
  • <StrictMode /> detects impurity through double-rendering
  • StrictMode only runs in development mode
  • ESLint plugin helps catch impure patterns
  • Always use useState for changing component data
  • Concurrent rendering can pause, resume, and skip renders
  • Same props must always produce same JSX output

React Rules: Side Effects

Overview

Pure functions must not have side effects. React expects component functions to be pure, but side effects are allowed in event handlers (and later in useEffect hook).

Core Concepts

What Are Side Effects?

Side effect: Any change to external state or environment outside the function.

Examples of side effects:

  • Modifying global variables
  • Interacting with DOM
  • Making API calls
  • Writing to localStorage
  • Console logging
  • Setting timers
  • Modifying function parameters

Pure Function Requirements

For a function to be pure, it must:

  1. Return same output for same input
  2. Have no side effects

❌ Impure function with side effect:

function sum(a, b) {
  const total = a + b
  window.localStorage.setItem('total', total) // ❌ Side effect!
  return total
}

The localStorage.setItem modifies external state, making this impure.

Why React Requires Purity

React needs freedom to:

  • Call components multiple times
  • Call components in any order
  • Pause and resume rendering
  • Skip unnecessary renders

Only possible if component functions are pure.

Rendering Phase vs Event Handlers

Rendering phase: Code that executes when React calls your component.

Event handlers: Code that executes when user interacts (clicks, types, etc).

Rule: Side effects NOT allowed in render phase, but ALLOWED in event handlers.

Impure Component (Side Effect in Render)

import { useState } from 'react'

function Counter() {
  const [counter, setCounter] = useState(0)

  function handleIncrement() {
    setCounter(counter + 1)
  }

  // ❌ Side effect in rendering phase
  window.localStorage.setItem('lastUpdate', new Date().getTime())

  return (
    <>
      <h1>Counter: {counter}</h1>
      <button onClick={handleIncrement}>Add 1</button>
    </>
  )
}

Why wrong: localStorage.setItem executes every time React calls Counter(), even during optimization passes.

Pure Component (Side Effect in Event)

import { useState } from 'react'

function Counter() {
  const [counter, setCounter] = useState(0)

  function handleIncrement() {
    setCounter(counter + 1)

    // ✅ Side effect in event handler
    window.localStorage.setItem('lastUpdate', new Date().getTime())
  }

  return (
    <>
      <h1>Counter: {counter}</h1>
      <button onClick={handleIncrement}>Add 1</button>
    </>
  )
}

Why correct: Side effect only runs when user clicks button, not during render.

Identifying Rendering Phase

Test: Imagine manually calling your component function - what executes?

function Counter() {
  const [counter, setCounter] = useState(0)

  // ✅ Rendering phase: executes when Counter() called
  const doubled = counter * 2

  function handleIncrement() {
    // ❌ NOT rendering phase: only when user clicks
    setCounter(counter + 1)
  }

  // ✅ Rendering phase: executes when Counter() called
  console.log(counter)

  return <button onClick={handleIncrement}>{counter}</button>
}

Event handler code only executes when event fires, not during render.

Usage Examples

Form submission with API call:

import { useState } from 'react'

function ContactForm() {
  const [name, setName] = useState('')
  const [email, setEmail] = useState('')

  function handleSubmit(event) {
    event.preventDefault()

    // ✅ Side effect (API call) in event handler
    fetch('/api/contact', {
      method: 'POST',
      body: JSON.stringify({ name, email }),
    })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <button type="submit">Submit</button>
    </form>
  )
}

Analytics tracking on interaction:

function ProductCard({ productId, name }) {
  function handleViewDetails() {
    // ✅ Side effect (analytics) in event handler
    window.analytics.track('product_viewed', {
      productId,
      timestamp: Date.now(),
    })

    // Navigate or show details...
  }

  return (
    <div>
      <h3>{name}</h3>
      <button onClick={handleViewDetails}>View Details</button>
    </div>
  )
}

localStorage on user action:

import { useState } from 'react'

function ThemeToggle() {
  const [isDark, setIsDark] = useState(false)

  function handleToggle() {
    const newTheme = !isDark
    setIsDark(newTheme)

    // ✅ Side effect (localStorage) in event handler
    localStorage.setItem('theme', newTheme ? 'dark' : 'light')
  }

  return <button onClick={handleToggle}>Toggle Theme</button>
}

Summary

  • Pure functions must have no side effects
  • Side effect = any change to external state/environment
  • Examples: DOM manipulation, API calls, localStorage, global variables
  • React component functions must be pure
  • Rendering phase = code executed when React calls component
  • Side effects NOT allowed in rendering phase
  • Side effects ARE allowed in event handlers
  • Event handlers only execute on user interaction, not during render
  • Later: useEffect hook enables side effects outside events
  • React needs purity to optimize rendering (pause, resume, skip)
  • Test: if you called Component() manually, what executes = render phase
  • Event handler code doesn't execute during render, only on interaction

Summary

Props Immutability:

  • Props are immutable - never modify them
  • Component must produce same output for same props
  • Use state variables with setState for changing values

Pure Functions:

  • Given same inputs, always produce same output
  • React expects components written as pure functions
  • Benefits: easier testing, concurrency-friendly
  • React uses concurrent rendering for performance

StrictMode:

  • Helps catch impure functions
  • Renders components twice in development mode
  • Production: single render
  • Use to verify component purity

Side Effects:

  • Any change to state or external environment from function execution
  • Pure functions have no side effects
  • Side effects NOT allowed in rendering phase (when React calls component)
  • Side effects allowed in event handlers (not called during render)
  • React has separate mechanisms for side effects (covered later)

State with Other Data Types

Overview

useState works with any data type. This lesson covers boolean and string state, including boolean operations and JSX rendering behavior.

Core Concepts

Boolean State

Flipping booleans with ! operator:

console.log(!true) // false
console.log(!false) // true

The ! operator negates (flips) boolean values.

Toggle button example:

import { useState } from 'react'

function App() {
  const [isActive, setIsActive] = useState(true)

  function handleActiveToggle() {
    setIsActive(!isActive) // Flip boolean
  }

  return <button onClick={handleActiveToggle}>Toggle state</button>
}

Flow:

  • Initial: isActive = true
  • First click: setIsActive(!true)setIsActive(false)
  • Second click: setIsActive(!false)setIsActive(true)
  • Third click: setIsActive(!true)setIsActive(false)

Naming Convention for Booleans

Follow useState naming convention with boolean prefixes:

const [isActive, setIsActive] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [hasError, setHasError] = useState(false)
const [canEdit, setCanEdit] = useState(true)

Booleans in JSX

JSX doesn't render boolean values:

function App() {
  const [isActive, setIsActive] = useState(true)

  // Nothing visible - JSX doesn't render true/false
  return <p>Value: {isActive}</p>
}

JSX ignores: true, false, null, undefined

Debug by converting to string:

<p>Value: {String(isActive)}</p> // Shows "true" or "false"

Display user-friendly text with ternary:

<p>Value: {isActive ? 'Activated' : 'Disabled'}</p>

Ternary operator: condition ? valueIfTrue : valueIfFalse

String State

Basic string state:

import { useState } from 'react'

function App() {
  const [userType, setUserType] = useState('editor')

  function handleEditorClick() {
    setUserType('editor')
  }

  function handleAdminClick() {
    setUserType('admin')
  }

  return (
    <>
      <p>User type: {userType}</p>
      <button onClick={handleEditorClick}>Make Editor</button>
      <button onClick={handleAdminClick}>Make Admin</button>
    </>
  )
}

Initial values:

const [name, setName] = useState('') // Empty string
const [status, setStatus] = useState('active') // Specific string

Usage Examples

Toggle visibility:

function Modal() {
  const [isVisible, setIsVisible] = useState(false)

  function handleToggle() {
    setIsVisible(!isVisible)
  }

  return (
    <>
      <button onClick={handleToggle}>
        {isVisible ? 'Hide' : 'Show'} Modal
      </button>
      {isVisible && (
        <div className="modal">
          <h2>Modal Content</h2>
          <button onClick={() => setIsVisible(false)}>Close</button>
        </div>
      )}
    </>
  )
}

Multiple state selection:

function Settings() {
  const [theme, setTheme] = useState('light')
  const [language, setLanguage] = useState('en')

  return (
    <div>
      <div>
        <p>Theme: {theme}</p>
        <button onClick={() => setTheme('light')}>Light</button>
        <button onClick={() => setTheme('dark')}>Dark</button>
        <button onClick={() => setTheme('auto')}>Auto</button>
      </div>
      <div>
        <p>Language: {language}</p>
        <button onClick={() => setLanguage('en')}>English</button>
        <button onClick={() => setLanguage('es')}>Español</button>
        <button onClick={() => setLanguage('fr')}>Français</button>
      </div>
    </div>
  )
}

Boolean flags with user feedback:

function LoginForm() {
  const [isLoading, setIsLoading] = useState(false)
  const [hasError, setHasError] = useState(false)

  function handleSubmit() {
    setIsLoading(true)
    setHasError(false)

    // Simulate API call
    setTimeout(() => {
      setIsLoading(false)
      setHasError(true) // Simulate error
    }, 2000)
  }

  return (
    <div>
      <button onClick={handleSubmit} disabled={isLoading}>
        {isLoading ? 'Loading...' : 'Login'}
      </button>
      {hasError && <p style={{ color: 'red' }}>Login failed</p>}
    </div>
  )
}

Summary

  • useState works with any data type (boolean, string, number, etc.)
  • Boolean state: use ! operator to flip values
  • Toggle pattern: setState(!state)
  • Naming: isActive, hasError, canEdit for booleans
  • JSX doesn't render true, false, null, undefined
  • Debug: convert to string with String(value)
  • Display: use ternary operator {bool ? "Yes" : "No"}
  • String state: useful for selections, user types, statuses
  • Initialize with "" (empty) or specific string value
  • Same useState syntax regardless of data type
  • Next lesson: conditional rendering with state

Conditional Rendering with State

Overview

Conditional rendering displays different UI based on state values. React supports multiple patterns: if statements, ternary operators, and logical && operator.

Core Concepts

Using if Statements

Return different JSX based on state:

import { useState } from 'react'

function App() {
  const [userType, setUserType] = useState('user')

  if (userType === 'admin') {
    return <h1>Admin dashboard</h1>
  }

  return <h1>Welcome user</h1>
}

When to use: Elements differ significantly.

Using Ternary Operator

Single return with inline condition:

import { useState } from 'react'

function App() {
  const [userType, setUserType] = useState('user')

  return <h1>{userType === 'admin' ? 'Admin dashboard' : 'Welcome user'}</h1>
}

Syntax: condition ? valueIfTrue : valueIfFalse

With JSX elements (use Fragments):

import { useState } from 'react'

function App() {
  const [userType, setUserType] = useState('user')

  return (
    <h1>
      {userType === 'admin' ? (
        <>
          <strong>Admin</strong> dashboard
        </>
      ) : (
        <>
          Welcome <em>user</em>
        </>
      )}
    </h1>
  )
}

Warning: Don't overuse - can hurt readability. Prefer if for complex logic.

Using Logical && Operator

Conditionally render JSX only when condition true:

import { useState } from 'react'

function App() {
  const [userType, setUserType] = useState('user')

  return (
    <>
      <h1>Welcome</h1>
      {userType === 'admin' && <p>You are logged in as an admin.</p>}
    </>
  )
}

How it works:

  • userType === "admin" evaluates first
  • If false: && quits, nothing rendered
  • If true: second expression evaluated, JSX rendered

Result:

  • When userType === "user": Only h1 rendered
  • When userType === "admin": Both h1 and p rendered

Multiple && Conditions

import { useState } from 'react'

function App() {
  const [userType, setUserType] = useState('user')

  return (
    <>
      <h1>Welcome</h1>
      {userType === 'admin' && <p>You are logged in as admin.</p>}
      {userType === 'user' && <p>Welcome back.</p>}
    </>
  )
}

Common Gotcha: Numbers

❌ Wrong - renders 0:

function App() {
  const count = 0

  return count && <p>You have mail</p>
}

Why: 0 is falsy, so && returns 0. JSX renders numbers, so 0 appears on page.

✅ Correct - use boolean condition:

function App() {
  const count = 0

  return count > 0 && <p>You have mail</p>
}

Always use comparison operators to ensure boolean result.

Usage Examples

Feature flags:

function App() {
  const [isPremium, setIsPremium] = useState(false)
  const [isLoggedIn, setIsLoggedIn] = useState(true)

  return (
    <>
      <h1>Dashboard</h1>
      {isLoggedIn && <p>Welcome back!</p>}
      {isPremium && <button>Access Premium Features</button>}
      {!isLoggedIn && <button>Please Log In</button>}
    </>
  )
}

Loading states:

function DataDisplay() {
  const [isLoading, setIsLoading] = useState(true)
  const [data, setData] = useState(null)

  if (isLoading) {
    return <p>Loading...</p>
  }

  if (!data) {
    return <p>No data found</p>
  }

  return <div>{data.content}</div>
}

Complex conditions with ternary:

function StatusBadge() {
  const [status, setStatus] = useState('pending')

  return (
    <span
      className={
        status === 'approved'
          ? 'badge-success'
          : status === 'rejected'
          ? 'badge-error'
          : 'badge-warning'
      }
    >
      {status === 'approved'
        ? '✓ Approved'
        : status === 'rejected'
        ? '✗ Rejected'
        : '⏳ Pending'}
    </span>
  )
}

Notification count:

function Notifications() {
  const [count, setCount] = useState(5)

  return (
    <div>
      <h2>Notifications</h2>
      {count > 0 && <p>You have {count} unread messages</p>}
      {count === 0 && <p>No new notifications</p>}
    </div>
  )
}

Summary

  • Three conditional rendering patterns: if, ternary ? :, logical &&
  • if statements: best for significantly different elements
  • Ternary operator: single return, inline conditions
  • Ternary with JSX: wrap in Fragments <>...</>
  • Logical &&: render element only when condition true
  • && evaluates first expression; if truthy, renders second
  • Gotcha: Always use boolean conditions with && (not numbers)
  • 0 is falsy but rendered by JSX - use count > 0, not just count
  • Don't overuse ternary - readability matters
  • Multiple && can handle different conditions independently
  • Comparison operators ensure boolean result: >, <, ===, !==

Summary

State with Different Data Types:

Boolean State:

  • useState(true) or useState(false)
  • Use ! operator to flip: setValue(!value)
  • Ternary operator for conditional rendering: value ? <A /> : <B />

String State:

  • useState("") for empty initial string
  • Common for form inputs and text data

JSX Rendering Rules:

  • JSX does NOT render: true, false, null, undefined
  • These values are valid but produce no output
  • Useful for conditional rendering patterns

Multiple State Variables

Overview

Components can have multiple state variables using multiple useState calls. React tracks each state by the stable order of hook calls, reinforcing why Rules of Hooks matter.

Core Concepts

Multiple useState Calls

Create multiple states by calling useState multiple times:

import { useState } from 'react'

function App() {
  const [enabled, setEnabled] = useState(true)
  const [count, setCount] = useState(0)

  function handleIncrement() {
    setCount(count + 1)
  }

  function handleToggle() {
    setEnabled(!enabled)
  }

  return (
    <>
      <p>Count is {count}</p>
      <button onClick={handleIncrement}>+ 1</button>
      <button onClick={handleToggle}>Toggle</button>
    </>
  )
}

Two independent states:

  • enabled (boolean, starts at true)
  • count (number, starts at 0)

Two independent setters:

  • setEnabled updates only enabled
  • setCount updates only count

How React Tracks Multiple States

First render:

  1. First useState(true) → returns [true, setEnabled]
  2. Second useState(0) → returns [0, setCount]

Second render (after update):

  1. First useState(true) → returns [false, setEnabled] (initial value ignored)
  2. Second useState(0) → returns [1, setCount] (initial value ignored)

Key insight: React uses calling order to track which state is which.

Why Rules of Hooks Matter

Order must be stable:

// ✅ Good - stable order
function App() {
  const [enabled, setEnabled] = useState(true)
  const [count, setCount] = useState(0)
  // ...
}

❌ Breaks React's tracking:

function App() {
  const [enabled, setEnabled] = useState(true)

  // ❌ Conditional hook - order not stable
  if (someCondition) {
    const [count, setCount] = useState(0)
  }
}

Why it breaks:

  • First render: 2 hook calls
  • Second render: 1 hook call (if condition false)
  • React can't match hooks between renders
  • Error thrown

Order doesn't matter, stability does:

// Both orders work, choose one and stick with it
function App() {
  const [count, setCount] = useState(0)
  const [enabled, setEnabled] = useState(true)
  // Works fine! Just keep same order every render
}

Initial Values Used Once

Initial values only apply on first render:

function App() {
  const [count, setCount] = useState(0) // 0 used only first time

  // After first render, useState(0) returns current state value
  // not the initial 0

  return <p>Count: {count}</p>
}

Renders:

  1. First: useState(0)count = 0
  2. After setCount(5): useState(0)count = 5 (ignores 0)
  3. After setCount(10): useState(0)count = 10 (ignores 0)

Usage Examples

User preferences:

function Settings() {
  const [theme, setTheme] = useState('light')
  const [fontSize, setFontSize] = useState(16)
  const [notifications, setNotifications] = useState(true)

  return (
    <div>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Theme: {theme}
      </button>
      <button onClick={() => setFontSize(fontSize + 2)}>
        Font: {fontSize}px
      </button>
      <button onClick={() => setNotifications(!notifications)}>
        Notifications: {notifications ? 'On' : 'Off'}
      </button>
    </div>
  )
}

Form state:

function LoginForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [rememberMe, setRememberMe] = useState(false)
  const [isLoading, setIsLoading] = useState(false)

  function handleSubmit() {
    setIsLoading(true)
    // Submit logic...
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <label>
        <input
          type="checkbox"
          checked={rememberMe}
          onChange={(e) => setRememberMe(e.target.checked)}
        />
        Remember me
      </label>
      <button disabled={isLoading}>{isLoading ? 'Loading...' : 'Login'}</button>
    </form>
  )
}

Component with mixed state types:

function ShoppingCart() {
  const [items, setItems] = useState([])
  const [total, setTotal] = useState(0)
  const [discount, setDiscount] = useState(null)
  const [isCheckoutOpen, setIsCheckoutOpen] = useState(false)

  return (
    <div>
      <p>Items: {items.length}</p>
      <p>Total: ${total}</p>
      {discount && <p>Discount: -{discount}%</p>}
      <button onClick={() => setIsCheckoutOpen(true)}>Checkout</button>
    </div>
  )
}

Summary

  • Multiple states: call useState multiple times in same component
  • Each state has independent setter function
  • React tracks states by calling order (stable order required)
  • Rules of Hooks prevent breaking state tracking
  • Initial values only used on first render
  • Subsequent renders ignore initial values, return current state
  • Order of useState calls must never change between renders
  • Conditional or loop-wrapped hooks break order stability
  • Order doesn't matter, but must be consistent
  • Each state variable and setter are independent
  • Next lesson: avoiding unnecessary states

Avoiding Unnecessary State Variables

Overview

Common pitfall: creating state variables for values that can be computed from existing states. This adds complexity, triggers unnecessary re-renders, and slows down apps.

Core Concepts

The Problem: Redundant State

❌ Wrong - unnecessary state:

import { useState } from 'react'

function App() {
  const [firstName, setFirstName] = useState('')
  const [lastName, setLastName] = useState('')
  // ❌ fullName can be computed - don't make it state
  const [fullName, setFullName] = useState('')

  function handleDemo() {
    setFirstName('Sam')
    setLastName('Blue')
    // ❌ Duplicates data and logic
    setFullName('Sam Blue')
  }

  return (
    <>
      <p>Your full name is {fullName}</p>
      <button onClick={handleDemo}>Demo name</button>
    </>
  )
}

Problems:

  1. Duplicated data (fullName duplicates first + last)
  2. Duplicated logic (manually keeping fullName in sync)
  3. Extra re-renders (3 setState calls instead of 2)
  4. More complex code (more states to manage)
  5. Bug-prone (easy to forget updating fullName)

The Solution: Computed Values

✅ Correct - compute from existing states:

import { useState } from 'react'

function App() {
  const [firstName, setFirstName] = useState('')
  const [lastName, setLastName] = useState('')

  function handleDemo() {
    setFirstName('Sam')
    setLastName('Blue')
    // No setFullName needed!
  }

  // ✅ Normal variable computed from states
  const fullName = `${firstName} ${lastName}`

  return (
    <>
      <p>Your full name is {fullName}</p>
      <button onClick={handleDemo}>Demo name</button>
    </>
  )
}

Benefits:

  1. Single source of truth (firstName and lastName)
  2. No manual synchronization
  3. Fewer re-renders (2 setState calls, not 3)
  4. Simpler code
  5. No risk of data inconsistency

Why It Works

Every render executes component function:

  1. firstName and lastName have current values
  2. fullName computed from current values
  3. Always correct, always in sync

Compute outside event handler:

function App() {
  const [firstName, setFirstName] = useState('')
  const [lastName, setLastName] = useState('')

  // ✅ Computed at top level, always correct
  const fullName = `${firstName} ${lastName}`

  function handleDemo() {
    setFirstName('Sam')
    setLastName('Blue')
  }

  return <p>{fullName}</p>
}

Computation is cheap, re-rendering is expensive.

Adding Logic with if Conditions

Conditional computation:

import { useState } from 'react'

function App() {
  const [firstName, setFirstName] = useState('')
  const [lastName, setLastName] = useState('')

  function handleDemo() {
    setFirstName('Sam')
    setLastName('Blue')
  }

  // ✅ Default value when states empty
  let fullName = 'N/A'
  if (firstName && lastName) {
    fullName = `${firstName} ${lastName}`
  }

  return (
    <>
      <p>Your full name is {fullName}</p>
      <button onClick={handleDemo}>Demo name</button>
    </>
  )
}

Result:

  • Before click: "Your full name is N/A"
  • After click: "Your full name is Sam Blue"

When to Use State vs Computed

Use state for:

  • User input (form fields)
  • Toggle states (on/off, open/closed)
  • API response data
  • Values that can't be derived

Use computed values for:

  • Derived from existing states
  • Formatted versions (full name from first + last)
  • Calculations (total from items and prices)
  • Filtered/sorted arrays

Usage Examples

Shopping cart total:

function ShoppingCart() {
  const [items, setItems] = useState([
    { id: 1, price: 10, quantity: 2 },
    { id: 2, price: 25, quantity: 1 },
  ])

  // ✅ Computed - don't create state for this
  const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0)

  return <p>Total: ${total}</p>
}

Filtered list:

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Buy milk', done: false },
    { id: 2, text: 'Walk dog', done: true },
  ])
  const [filter, setFilter] = useState('all') // "all", "active", "done"

  // ✅ Computed - filters based on existing states
  const filteredTodos = todos.filter((todo) => {
    if (filter === 'active') return !todo.done
    if (filter === 'done') return todo.done
    return true
  })

  return (
    <div>
      {filteredTodos.map((todo) => (
        <p key={todo.id}>{todo.text}</p>
      ))}
    </div>
  )
}

Form validation:

function SignupForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [confirmPassword, setConfirmPassword] = useState('')

  // ✅ All computed - no state needed
  const isEmailValid = email.includes('@')
  const isPasswordLongEnough = password.length >= 8
  const doPasswordsMatch = password === confirmPassword
  const isFormValid = isEmailValid && isPasswordLongEnough && doPasswordsMatch

  return (
    <form>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      {!isEmailValid && <p>Invalid email</p>}

      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      {!isPasswordLongEnough && <p>Password too short</p>}

      <input
        type="password"
        value={confirmPassword}
        onChange={(e) => setConfirmPassword(e.target.value)}
      />
      {!doPasswordsMatch && <p>Passwords don't match</p>}

      <button disabled={!isFormValid}>Sign Up</button>
    </form>
  )
}

Summary

  • Don't create state variables for computed values
  • Compute from existing states using normal variables
  • Benefits: simpler code, fewer re-renders, no synchronization bugs
  • Every render recomputes values with current state
  • Computation is fast, state management is complex
  • Use let and if for conditional computed values
  • State for: user input, toggles, API data
  • Computed for: derived values, calculations, formatting, filtering
  • Single source of truth prevents data inconsistency
  • Next lesson: batched state updates

Summary

Multiple State Variables:

  • Call useState multiple times in component
  • React tracks state values by order of useState calls
  • Rules of hooks necessary to keep order stable
  • Default value only used on first render

State Management Best Practices:

  • Avoid unnecessary state variables
  • Compute values from existing state when possible
  • Create normal variables using state variables
  • Don't store computed values in separate state

Example:

const [count, setCount] = useState(0)
const doubled = count * 2 // Computed, not separate state

Why Order Matters:

  • React relies on call order to match state across renders
  • Hooks must be called in same order every render
  • Cannot conditionally call hooks

Batched State Updates

Overview

React batches multiple setState calls into a single re-render for performance. State values remain unchanged during the current render, updating only after the function completes.

Core Concepts

State Updates in Next Render

State value doesn't change immediately:

import { useState } from 'react'

function App() {
  const [counter, setCounter] = useState(0)

  function handleClick() {
    console.log(counter) // Logs: 0
    setCounter(counter + 1)
    console.log(counter) // Still logs: 0 (not 1!)
  }

  return <button onClick={handleClick}>+ 1</button>
}

Why both log 0:

  • counter is 0 throughout entire function execution
  • setCounter schedules update for next render
  • Current render keeps old value

Accessing New Value

Store new value in variable if needed:

import { useState } from 'react'

function App() {
  const [counter, setCounter] = useState(0)

  function handleClick() {
    const nextCounter = counter + 1
    console.log(counter) // Logs: 0 (current)
    setCounter(nextCounter)
    console.log(nextCounter) // Logs: 1 (next)
  }

  return <button onClick={handleClick}>+ 1</button>
}

Common names: nextCounter, nextState, newValue

Multiple setState Calls: Single Re-render

React batches updates:

import { useState } from 'react'

function App() {
  const [firstName, setFirstName] = useState('')
  const [lastName, setLastName] = useState('')

  function handleDemo() {
    setFirstName('Sam') // Schedules re-render
    setLastName('Blue') // Schedules re-render
  } // React re-renders ONCE after function completes

  return (
    <>
      <p>
        Your full name is {firstName} {lastName}
      </p>
      <button onClick={handleDemo}>Demo name</button>
    </>
  )
}

How batching works:

  1. setFirstName("Sam") called → React schedules re-render
  2. Function continues executing
  3. setLastName("Blue") called → React schedules another re-render
  4. Function completes
  5. React performs single re-render with both updates

Benefits:

  • Fewer re-renders = better performance
  • All related updates happen together
  • No intermediate UI states

Evolution: React 18 Batching

React 17 and earlier:

  • Batched updates in event handlers only
  • Separate re-renders in setTimeout, promises, async functions

React 18 and later:

  • Batches all updates everywhere
  • Works with setTimeout, promises, async/await
  • Automatic performance improvement
function App() {
  const [count, setCount] = useState(0)
  const [text, setText] = useState('')

  function handleClick() {
    setTimeout(() => {
      // React 17: 2 separate re-renders
      // React 18: 1 batched re-render
      setCount((c) => c + 1)
      setText('Updated')
    }, 1000)
  }

  return <button onClick={handleClick}>Update</button>
}

Key Insight

State is a snapshot:

function App() {
  const [count, setCount] = useState(0)

  function handleClick() {
    // All uses of `count` see same value (snapshot from this render)
    console.log(count) // 0
    const double = count * 2 // 0 * 2 = 0
    setCount(count + 1)
    console.log(count) // Still 0
    console.log(double) // Still 0
  }

  return <button onClick={handleClick}>Count: {count}</button>
}

State variables are constant for the entire render.

Usage Examples

Multiple related updates:

function UserProfile() {
  const [name, setName] = useState('')
  const [email, setEmail] = useState('')
  const [age, setAge] = useState(0)
  const [isLoading, setIsLoading] = useState(false)

  function handleLoadUser() {
    setIsLoading(true)

    // Simulated API call
    setTimeout(() => {
      // All 4 updates batched into 1 re-render (React 18)
      setName('Sam Blue')
      setEmail('[email protected]')
      setAge(28)
      setIsLoading(false)
    }, 1000)
  }

  return <button onClick={handleLoadUser}>Load User</button>
}

Accessing new value during render:

function Calculator() {
  const [total, setTotal] = useState(0)

  function handleAdd(amount) {
    const newTotal = total + amount
    setTotal(newTotal)

    // Use newTotal immediately
    console.log(`Added ${amount}, new total: ${newTotal}`)

    if (newTotal > 100) {
      alert('Total exceeded 100!')
    }
  }

  return (
    <div>
      <p>Total: {total}</p>
      <button onClick={() => handleAdd(10)}>+10</button>
      <button onClick={() => handleAdd(50)}>+50</button>
    </div>
  )
}

Form submission with multiple states:

function ContactForm() {
  const [name, setName] = useState('')
  const [message, setMessage] = useState('')
  const [submitted, setSubmitted] = useState(false)

  function handleSubmit() {
    // All updates batched into single re-render
    console.log('Submitting:', name, message)
    setName('')
    setMessage('')
    setSubmitted(true)
  }

  return submitted ? (
    <p>Thanks!</p>
  ) : (
    <form onSubmit={handleSubmit}>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <textarea value={message} onChange={(e) => setMessage(e.target.value)} />
      <button>Submit</button>
    </form>
  )
}

Summary

  • State values don't change during current render (function execution)
  • setState schedules update for next render
  • State is snapshot - constant throughout render
  • Store new value in variable if needed: const next = state + 1
  • Multiple setState calls batch into single re-render
  • Batching improves performance (fewer re-renders)
  • React 18: batches all updates everywhere (events, timeouts, promises)
  • React 17: batched only in event handlers
  • Built-in feature, no configuration needed
  • Next lesson: state updater functions for multiple updates

State Updater Functions

Overview

When updating state multiple times in one function, batching can cause unexpected results. State updater functions solve this by providing the latest state value for each update.

Core Concepts

The Problem: Batched Updates with Stale State

Doesn't work as expected:

import { useState } from 'react'

function Counter(props) {
  const [count, setCount] = useState(0)

  function handleClick() {
    setCount(count + 1)
    if (props.bonus) {
      setCount(count + 1)
    }
  }

  return (
    <>
      <p>Count {count}</p>
      <button onClick={handleClick}>+ 2</button>
    </>
  )
}

Expected: Increment by 2 when props.bonus is true Actual: Only increments by 1

Why it fails:

  1. Start: count = 0
  2. User clicks button
  3. setCount(count + 1) → schedules update to 1 (but count still 0)
  4. setCount(count + 1) → schedules update to 1 (still using stale count = 0)
  5. Both updates set count to same value: 1

Both setCount calls use stale count value from current render.

The Solution: State Updater Function

Pass function instead of value:

function handleClick() {
  setCount((c) => c + 1)
  if (props.bonus) {
    setCount((c) => c + 1)
  }
}

Arrow function syntax:

  • c => c + 1 is short for:
function(c) {
    return c + 1;
}

How it works:

  1. First setCount(c => c + 1):

    • React calls function with current value: c = 0
    • Returns 0 + 1 = 1
    • Next state: 1
  2. Second setCount(c => c + 1):

    • React calls function with latest value: c = 1
    • Returns 1 + 1 = 2
    • Next state: 2

React executes updater functions sequentially, passing result of each to the next.

Naming Conventions

First letter:

setCount((c) => c + 1)
setEnabled((e) => !e)
setIndex((i) => i + 1)

Current prefix:

setCount((currentCount) => currentCount + 1)
setEnabled((currentEnabled) => !currentEnabled)

Avoid using same name:

// ❌ Confusing - shadowing state variable
setCount((count) => count + 1)

// ✅ Better - distinct names
setCount((c) => c + 1)
setCount((currentCount) => currentCount + 1)

When to Use Updater Functions

Use updater function when:

  • Updating same state multiple times in one function
  • Next update depends on previous update
  • Working with previous state value

Normal value when:

  • Single update
  • Setting to specific value (not based on previous)
  • Independent of current state
// Multiple updates - use updater
setCount((c) => c + 1)
setCount((c) => c + 1)

// Single update - normal value fine
setCount(10)
setCount(count + 5)

Usage Examples

Multiple increments:

function Counter() {
  const [count, setCount] = useState(0)

  function handleTripleClick() {
    // All three execute in order
    setCount((c) => c + 1) // 0 → 1
    setCount((c) => c + 1) // 1 → 2
    setCount((c) => c + 1) // 2 → 3
  }

  return <button onClick={handleTripleClick}>+3</button>
}

Conditional multiple updates:

function ScoreBoard({ isBonus, isPenalty }) {
  const [score, setScore] = useState(0)

  function handleScore() {
    setScore((s) => s + 10)

    if (isBonus) {
      setScore((s) => s + 5) // Uses updated value
    }

    if (isPenalty) {
      setScore((s) => s - 3) // Uses most recent value
    }
  }

  return <button onClick={handleScore}>Score</button>
}

Toggle with multiple operations:

function MultiToggle() {
  const [value, setValue] = useState(0)

  function handleComplexUpdate() {
    setValue((v) => v + 1) // 0 → 1
    setValue((v) => v * 2) // 1 → 2
    setValue((v) => v + 10) // 2 → 12
  }

  return <button onClick={handleComplexUpdate}>Update</button>
}

Array operations:

function TodoList() {
  const [todos, setTodos] = useState([])

  function handleAddMultiple() {
    setTodos((current) => [...current, 'Task 1'])
    setTodos((current) => [...current, 'Task 2'])
    setTodos((current) => [...current, 'Task 3'])
    // Ends with 3 tasks
  }

  return <button onClick={handleAddMultiple}>Add 3 Tasks</button>
}

Real-world: Undo/Redo stack:

function Editor() {
  const [history, setHistory] = useState([])

  function handleSave(content) {
    setHistory((h) => [...h, content])

    // Keep only last 10 entries
    setHistory((h) => h.slice(-10))
  }

  return <button onClick={() => handleSave('New content')}>Save</button>
}

Summary

  • Batched updates can use stale state values
  • Multiple setState(value) with same base value doesn't work as expected
  • State updater function: setState(current => newValue)
  • React calls updater functions sequentially
  • Each updater receives result of previous updater
  • Guarantees correct state value for multiple updates
  • Naming: use first letter (c => c + 1) or current prefix
  • Avoid shadowing state variable name
  • Use updater when: multiple updates, dependent updates
  • Use normal value when: single update, specific value
  • Not common but essential when needed
  • Still batched into single re-render

Summary

State Batching:

  • React batches multiple state updates together
  • Component re-renders only once per batch (not after each setState)
  • As of React 18: batching works in setTimeout and promises too

State Value in Current Render:

  • State value always reflects old value in current render
  • Current function call sees state before updates
  • Save new state in variable if needed in same render

Multiple Updates Issue:

  • Updating same state multiple times in function can cause issues
  • Relying on current state value leads to unexpected results
  • State updates are queued, not immediate

State Updater Function:

  • Use updater function for multiple state updates in single function
  • Pattern: setState(prevState => prevState + 1)
  • Guarantees correct current state value
  • Receives latest state as argument
  • Solves race condition with multiple updates

Example:

// ❌ Wrong - uses stale state
setCount(count + 1)
setCount(count + 1) // Still uses old count

// ✅ Correct - uses updater function
setCount((prev) => prev + 1)
setCount((prev) => prev + 1) // Uses updated value

JavaScript Arrays and Objects

Overview

Arrays and objects behave differently than primitives (numbers, strings, booleans) in JavaScript. Understanding reference comparison is essential for working with React state.

Core Concepts

Primitive Comparisons

Primitives compare by value:

1 === 1 // true
'hello world' === 'hello world' // true
true === true // true
false === false // true

Same values → true

Array and Object Comparisons

Arrays and objects compare by reference, not value:

[] === [];                                // false
{} === {};                                 // false
[10] === [10];                            // false
{name: "Alex"} === {name: "Alex"};        // false

Even with == operator, still false.

Why False?

Arrays and objects are reference types. Each literal creates new instance:

new Array() === new Array() // false
new Object() === new Object() // false

Equivalent notations:

  • [] same as new Array()
  • {} same as new Object()

Every creation makes new instance:

const arr1 = new Array()
arr1.push(10)
const arr2 = new Array()
arr2.push(10)
arr1 === arr2 // false - different instances

const obj1 = new Object()
obj1.name = 'Alex'
const obj2 = new Object()
obj2.name = 'Alex'
obj1 === obj2 // false - different instances

Though values identical, instances different → comparison false.

Reference Assignment

Assigning creates reference, not copy:

const firstArray = []
const secondArray = firstArray // Reference, not copy

console.log(firstArray) // []
console.log(secondArray) // []

firstArray.push(10)
console.log(firstArray) // [10]
console.log(secondArray) // [10] - also changed!

Why both change:

  • secondArray = firstArray creates reference
  • Both variables point to same location in memory
  • Changing one affects both

Performance feature:

  • Avoids duplicating large objects/arrays
  • But creates pitfall if unaware of references

Key Differences Table

TypeComparisonAssignment
PrimitivesBy valueBy value (copy)
Objects/ArraysBy referenceBy reference (same memory)

Usage Examples

Primitive behavior:

let a = 5
let b = a // Copies value
b = 10
console.log(a) // 5 - unchanged
console.log(b) // 10

Array reference behavior:

const arr1 = [1, 2, 3]
const arr2 = arr1 // Reference
arr2.push(4)
console.log(arr1) // [1, 2, 3, 4] - changed!
console.log(arr2) // [1, 2, 3, 4]

Object reference behavior:

const user1 = { name: 'Sam' }
const user2 = user1 // Reference
user2.age = 25
console.log(user1) // {name: "Sam", age: 25} - changed!
console.log(user2) // {name: "Sam", age: 25}

Same reference comparison:

const arr1 = [1, 2, 3]
const arr2 = arr1
console.log(arr1 === arr2) // true - same reference

const arr3 = [1, 2, 3]
console.log(arr1 === arr3) // false - different instances

Summary

  • Arrays are objects in JavaScript
  • [] === [] is false (compares references, not values)
  • {} === {} is false (compares references, not values)
  • [] === [] equivalent to new Array() === new Array()
  • {} === {} equivalent to new Object() === new Object()
  • Each literal/constructor creates new instance
  • Assignment creates reference, not copy
  • const b = a (where a is object/array) makes b point to same memory
  • Changes via either variable affect both
  • Performance feature but potential pitfall
  • Primitives (numbers, strings, booleans) compare by value
  • Objects/arrays compare by reference
  • Essential for understanding React state immutability

Why React Requires Immutability

Overview

React detects state changes by comparing references. Mutating arrays/objects doesn't change references, preventing re-renders. Immutability ensures React can detect changes.

Core Concepts

Mutation Doesn't Change Reference

const firstArray = []
const secondArray = firstArray
console.log(firstArray === secondArray) // true

firstArray.push(10)
console.log(firstArray === secondArray) // still true!

Problem: .push() mutates array but reference stays same.

What is Mutation?

Mutation: Changing existing value in place.

Mutating methods:

  • array.push()
  • array.pop()
  • array.splice()
  • array.sort()
  • object.property = value

These modify original without creating new reference.

React's State Comparison

React compares previous state to current state:

  • Different references → re-render
  • Same reference → no re-render

Example that breaks React:

const [items, setItems] = useState([1, 2, 3])

function handleAdd() {
  items.push(4) // ❌ Mutates - React won't detect change
  setItems(items) // Same reference
}

Reference unchanged → React skips re-render → UI doesn't update.

Why Not Check Content?

Could React check array/object contents?

Yes, but too expensive:

  • Deep comparison requires checking every property
  • Recursive for nested objects
  • Slow for large data structures

Solution: Reference comparison (fast, efficient).

All front-end frameworks use reference comparison, not just React.

Immutability Solution

Immutability: Create new values instead of modifying existing.

const firstArray = [1, 2, 3]
const secondArray = [...firstArray, 4] // New array

console.log(firstArray === secondArray) // false
console.log(firstArray) // [1, 2, 3] - unchanged
console.log(secondArray) // [1, 2, 3, 4] - new array

New reference → React detects change → re-render happens.

Primitives Don't Have This Problem

Primitives always compared by value:

const [count, setCount] = useState(0)

function handleClick() {
  setCount(count + 1) // ✅ Always works
}

count + 1 creates new number value, not reference.

Why primitives work:

  • Numbers, strings, booleans are immutable by nature
  • Cannot mutate 5 to become 6
  • Always create new value

Object.is() vs ===

React uses Object.is() instead of ===:

Object.is(5, 5) // true
Object.is({}, {}) // false (like ===)
Object.is(NaN, NaN) // true (unlike ===)
Object.is(0, -0) // false (unlike ===)

Differences:

  • NaN === NaN → false, but Object.is(NaN, NaN) → true
  • 0 === -0 → true, but Object.is(0, -0) → false

For most cases: Object.is() and === behave identically.

Usage Examples

Wrong - mutation:

function TodoApp() {
  const [todos, setTodos] = useState([])

  function handleAdd(text) {
    // ❌ Mutates todos array
    todos.push({ id: Date.now(), text })
    setTodos(todos) // Same reference - won't re-render!
  }

  return <button onClick={() => handleAdd('Buy milk')}>Add</button>
}

Correct - immutable:

function TodoApp() {
  const [todos, setTodos] = useState([])

  function handleAdd(text) {
    // ✅ Creates new array
    setTodos([...todos, { id: Date.now(), text }])
  }

  return <button onClick={() => handleAdd('Buy milk')}>Add</button>
}

Object mutation problem:

function UserProfile() {
  const [user, setUser] = useState({ name: 'Sam', age: 25 })

  function handleBirthday() {
    // ❌ Mutates user object
    user.age = user.age + 1
    setUser(user) // Same reference - won't re-render!
  }

  return <button onClick={handleBirthday}>Birthday</button>
}

Object immutable solution:

function UserProfile() {
  const [user, setUser] = useState({ name: 'Sam', age: 25 })

  function handleBirthday() {
    // ✅ Creates new object
    setUser({ ...user, age: user.age + 1 })
  }

  return <button onClick={handleBirthday}>Birthday</button>
}

Summary

  • Mutation changes value without changing reference
  • .push(), .pop(), .splice() mutate arrays
  • Direct property assignment mutates objects
  • React compares state by reference (not content)
  • Same reference → no re-render (even if content changed)
  • Different reference → re-render
  • Checking content would be too expensive for performance
  • All front-end frameworks use reference comparison
  • Immutability: create new values instead of modifying
  • Primitives don't have this problem (always compared by value)
  • React uses Object.is() (similar to ===)
  • Next chapters: techniques for immutable array/object operations

Summary

JavaScript Reference Equality:

  • Arrays and objects are reference types
  • [] === [] is false (different references)
  • {} === {} is false (different references)
  • Assigning variable to array/object references it (doesn't copy)

Array/Object Mutation:

  • Array.prototype.push() mutates array
  • Mutating doesn't trigger React re-render (reference unchanged)
  • React uses Object.is() (similar to ===) to compare state

Immutability Concept:

  • Create new values instead of modifying existing ones
  • Required for React state management
  • Must learn non-mutating manipulation techniques
  • Foundation for working with arrays/objects in state

State with Arrays: Adding Items

Overview

When updating array state in React, avoid mutating methods like .push(). Instead, use spread operator to create shallow copies, ensuring React detects changes.

Core Concepts

Why Not .push()?

// ❌ Don't use .push() - mutates array
const array = [1, 2]
array.push(3) // Mutates original
// React won't detect change

Mutation doesn't change reference → React misses update.

Shallow Copy with Spread Operator

Create new array with spread ...:

const array1 = [10]
const array2 = [...array1] // Shallow copy

array1[0] = 20
console.log(array1) // [20]
console.log(array2) // [10] - unchanged

Syntax breakdown of [...array1]:

  1. [] creates new array
  2. ...array1 spreads items from array1
  3. Items copied into new array
  4. Result: new array with copied items

"Shallow" explained later (2 chapters ahead).

Adding Item at End

const array1 = [10]
const array2 = [...array1, 20]

console.log(array1) // [10] - unchanged
console.log(array2) // [10, 20]

How [...array1, 20] works:

  1. Create new array
  2. Spread existing items: 10
  3. Add new item: 20
  4. Result: [10, 20]

Adding Item at Beginning

const array1 = [10, 15]
const array2 = [5, ...array1]

console.log(array1) // [10, 15] - unchanged
console.log(array2) // [5, 10, 15]

How [5, ...array1] works:

  1. Create new array
  2. Add new item: 5
  3. Spread existing items: 10, 15
  4. Result: [5, 10, 15]

State with Arrays

import { useState } from 'react'

function App() {
  const [values, setValues] = useState([])

  function handleAdd() {
    // Create shallow copy and add 5
    setValues([...values, 5])
  }

  return (
    <>
      <p>You have {values.length} items.</p>
      <button onClick={handleAdd}>Add 5</button>
    </>
  )
}

Flow:

  1. Initial: values = []
  2. Click: setValues([...[], 5])setValues([5])
  3. Re-render: values = [5]
  4. Click: setValues([...[ 5], 5])setValues([5, 5])
  5. Re-render: values = [5, 5]

Usage Examples

Todo list (add at end):

function TodoList() {
  const [todos, setTodos] = useState([])

  function handleAddTodo(text) {
    setTodos([...todos, { id: Date.now(), text }])
  }

  return (
    <div>
      <button onClick={() => handleAddTodo('Buy milk')}>Add Todo</button>
      <p>{todos.length} todos</p>
    </div>
  )
}

Notifications (add at beginning):

function Notifications() {
  const [notifications, setNotifications] = useState([])

  function handleNewNotification(message) {
    // New notifications appear first
    setNotifications([{ id: Date.now(), message }, ...notifications])
  }

  return (
    <div>
      <button onClick={() => handleNewNotification('New message!')}>
        Add Notification
      </button>
      {notifications.map((notif) => (
        <p key={notif.id}>{notif.message}</p>
      ))}
    </div>
  )
}

Shopping cart:

function ShoppingCart() {
  const [items, setItems] = useState([])

  function handleAddItem(product) {
    setItems([
      ...items,
      {
        id: Date.now(),
        name: product.name,
        price: product.price,
        quantity: 1,
      },
    ])
  }

  return (
    <button onClick={() => handleAddItem({ name: 'Laptop', price: 999 })}>
      Add to Cart
    </button>
  )
}

Counter with history:

function Counter() {
  const [count, setCount] = useState(0)
  const [history, setHistory] = useState([0])

  function handleIncrement() {
    const nextCount = count + 1
    setCount(nextCount)
    setHistory([...history, nextCount]) // Track history
  }

  return (
    <div>
      <p>Count: {count}</p>
      <p>History: {history.join(', ')}</p>
      <button onClick={handleIncrement}>+1</button>
    </div>
  )
}

Multiple items at once:

function BulkAdd() {
  const [numbers, setNumbers] = useState([])

  function handleAddMultiple() {
    setNumbers([...numbers, 1, 2, 3, 4, 5])
  }

  return <button onClick={handleAddMultiple}>Add 5 numbers</button>
}

Summary

  • Never use .push() to update React state (mutates array)
  • Use spread operator ... to create shallow copy
  • [...array] creates new array with copied items
  • Add at end: [...oldArray, newItem]
  • Add at beginning: [newItem, ...oldArray]
  • Can add multiple items: [...old, item1, item2, item3]
  • New array reference → React detects change → re-render
  • Pattern works with any array data (numbers, strings, objects)
  • Original state always unchanged (immutability)
  • Next lesson: removing and updating array items

State with Arrays: Other Operations

Overview

Beyond adding items, array state requires immutable methods for removing and updating elements. Use .filter(), .slice(), and .map() to avoid mutations.

Core Concepts

Removing with .filter()

.filter() creates new array, doesn't mutate:

import { useState } from 'react'

function App() {
  const [items, setItems] = useState(['Apple', 'Carrot', 'Banana'])

  function handleRemoveCarrots() {
    setItems(items.filter((item) => item !== 'Carrot'))
  }

  return <button onClick={handleRemoveCarrots}>Remove Carrots</button>
}

How it works:

  • items.filter(item => item !== "Carrot") keeps only non-"Carrot" items
  • Returns new array: ["Apple", "Banana"]
  • Original items unchanged

Keep only specific items:

function handleKeepOnlyCarrots() {
  setItems(items.filter((item) => item === 'Carrot'))
}

Removing with .slice()

.slice(start, end) creates copy excluding items:

Remove last item:

import { useState } from 'react'

function App() {
  const [items, setItems] = useState(['Apple', 'Carrot', 'Banana'])

  function handleRemoveLast() {
    setItems(items.slice(0, -1))
  }

  return <button onClick={handleRemoveLast}>Remove last</button>
}

items.slice(0, -1):

  • Start: index 0
  • End: -1 (last index from end)
  • Stops before end → excludes last item
  • Result: ["Apple", "Carrot"]

Common .slice() patterns:

CodeResultExplanation
items.slice(1)All except firstStart at index 1, no end specified
items.slice(2)All except first twoStart at index 2, no end specified
items.slice(0, 1)Only first itemIndex 0, stop before index 1
items.slice(0, 2)First two itemsIndex 0-1, stop before index 2
items.slice(0, -1)All except lastIndex 0, stop before last index
items.slice(0, -2)All except last twoIndex 0, stop before 2nd last index

Important: Use .slice(), not .splice() (with p). .splice() mutates original array.

Updating with .map()

.map() transforms array, returns new copy:

import { useState } from 'react'

function App() {
  const [items, setItems] = useState(['Apple', 'Carrot', 'Banana'])

  function handleChangeCarrots() {
    setItems(
      items.map((item) => {
        if (item === 'Carrot') {
          return 'Peas'
        }
        return item
      }),
    )
  }

  return <button onClick={handleChangeCarrots}>Change Carrots</button>
}

How .map() works:

  • Iterates each item
  • For "Carrot": returns "Peas"
  • For others: returns unchanged
  • Result: ["Apple", "Peas", "Banana"]

Must always return: Return item for unchanged items, new value for modified.

Alternative: Shallow Copy Then Modify

Create copy, then apply mutations:

import { useState } from 'react'

function App() {
  const [items, setItems] = useState(['Apple', 'Carrot', 'Banana'])

  function changeSecondItem() {
    const newItems = [...items] // Shallow copy
    newItems[1] = 'Peas' // Mutate copy (safe)
    setItems(newItems)
  }

  return <button onClick={changeSecondItem}>Change second item</button>
}

Why safe: Mutating newItems (copy), not original items.

Works for operations like:

  • Index-based updates: newItems[index] = value
  • .reverse(): newItems.reverse()
  • .sort(): newItems.sort()

Usage Examples

Remove by ID:

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Buy milk' },
    { id: 2, text: 'Walk dog' },
    { id: 3, text: 'Code' },
  ])

  function handleRemove(id) {
    setTodos(todos.filter((todo) => todo.id !== id))
  }

  return (
    <div>
      {todos.map((todo) => (
        <div key={todo.id}>
          {todo.text}
          <button onClick={() => handleRemove(todo.id)}>×</button>
        </div>
      ))}
    </div>
  )
}

Toggle done status:

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Buy milk', done: false },
    { id: 2, text: 'Walk dog', done: true },
  ])

  function handleToggle(id) {
    setTodos(
      todos.map((todo) => {
        if (todo.id === id) {
          return { ...todo, done: !todo.done }
        }
        return todo
      }),
    )
  }

  return (
    <div>
      {todos.map((todo) => (
        <div key={todo.id}>
          <input
            type="checkbox"
            checked={todo.done}
            onChange={() => handleToggle(todo.id)}
          />
          {todo.text}
        </div>
      ))}
    </div>
  )
}

Remove first N items:

function MessageQueue() {
  const [messages, setMessages] = useState([1, 2, 3, 4, 5])

  function handleProcess() {
    setMessages(messages.slice(2)) // Remove first 2
  }

  return <button onClick={handleProcess}>Process 2 Messages</button>
}

Update by index:

function ScoreBoard() {
  const [scores, setScores] = useState([10, 20, 30])

  function handleIncrementScore(index) {
    const newScores = [...scores]
    newScores[index] = newScores[index] + 5
    setScores(newScores)
  }

  return (
    <div>
      {scores.map((score, index) => (
        <button key={index} onClick={() => handleIncrementScore(index)}>
          Score {index}: {score}
        </button>
      ))}
    </div>
  )
}

Reverse array:

function OrderList() {
  const [items, setItems] = useState(['First', 'Second', 'Third'])

  function handleReverse() {
    const newItems = [...items]
    newItems.reverse()
    setItems(newItems)
  }

  return <button onClick={handleReverse}>Reverse</button>
}

Summary

  • Remove: Use .filter() for condition-based removal
  • .filter(item => condition) keeps items matching condition
  • Remove by index: Use .slice(start, end)
  • .slice() creates new array, .splice() mutates (avoid)
  • Common patterns: .slice(1) (skip first), .slice(0, -1) (skip last)
  • Update: Use .map() to transform items
  • .map() must return value for every item (unchanged or modified)
  • Alternative: Create shallow copy [...array], then mutate copy
  • Mutating copy is safe (original unchanged)
  • All methods return new arrays (immutable operations)
  • Choose method based on operation: filter (remove), slice (index), map (transform)

Summary

Avoiding Array Mutation:

  • Must avoid mutating arrays in React state
  • Create shallow copies using spread operator (...)

Adding Items:

  • End of array: [...state, newItem]
  • Beginning of array: [newItem, ...state]

Removing Items:

  • .filter() - creates new array with elements passing condition
  • .slice(start, end) - creates new array with elements between indexes

Updating Items:

  • Use .map() method to transform elements
  • Returns new array with updated elements

Alternative Approach:

  • Create shallow copy then edit: const copy = [...state]
  • Useful for complex operations
  • Still creates new array (doesn't mutate original)

State with Objects: Add and Update Properties

Overview

Like arrays, objects in React state must be updated immutably using spread operator. This creates new object references, enabling React to detect changes.

Core Concepts

Shallow Copy of Objects

Use spread operator {...} to copy objects:

const data = {
  id: 1,
  name: 'Sam',
}

// Create shallow copy
const newObj = { ...data }
newObj.age = 20

console.log(newObj) // {id: 1, name: "Sam", age: 20}
console.log(data) // {id: 1, name: "Sam"} - unchanged

{...data} spreads key/value pairs into new object.

Adding Property

Spread existing properties, add new:

const data = {
  id: 1,
  name: 'Sam',
}

const newObj = { ...data, age: 20 }

console.log(newObj) // {id: 1, name: "Sam", age: 20}
console.log(data) // {id: 1, name: "Sam"} - unchanged

Syntax {...data, age: 20}:

  1. Create new object
  2. Spread existing key/value pairs
  3. Add age: 20

Since data had no age, property added.

Updating Existing Property

Later properties override earlier ones:

const data = {
  id: 1,
  name: 'Sam',
  age: 18,
}

const newObj = { ...data, age: 20 }

console.log(newObj) // {id: 1, name: "Sam", age: 20}
console.log(data) // {id: 1, name: "Sam", age: 18} - unchanged

How {...data, age: 20} creates object:

{
    id: 1,       // from data
    name: "Sam", // from data
    age: 18,     // from data
    age: 20      // overwrites previous age
}

Final: {id: 1, name: "Sam", age: 20}

Order Matters

Spread before or after determines override:

const data = { age: 18 }

// Spread first, override after
const newObj1 = { ...data, age: 20 }
console.log(newObj1) // {age: 20}

// Override first, spread after (overwrites override)
const newObj2 = { age: 20, ...data }
console.log(newObj2) // {age: 18}

Rule: Later properties win.

State with Objects

import { useState } from 'react'

function App() {
  const [user, setUser] = useState({ id: 1, name: 'Sam' })

  function handleClick() {
    setUser({ ...user, age: 20 })
  }

  return <button onClick={handleClick}>Set age to 20</button>
}

Flow:

  1. Initial: user = {id: 1, name: "Sam"}
  2. Click: setUser({...user, age: 20})
  3. Creates: {id: 1, name: "Sam, age: 20}
  4. Re-render: user = {id: 1, name: "Sam", age: 20}

Incrementing Property

Use existing value in new object:

import { useState } from 'react'

function App() {
  const [user, setUser] = useState({ id: 1, name: 'Sam', age: 20 })

  function handleClick() {
    setUser({ ...user, age: user.age + 1 })
  }

  return <button onClick={handleClick}>Increment age</button>
}

How {...user, age: user.age + 1} works:

  1. Spread existing: id: 1, name: "Sam", age: 20
  2. Calculate new age: user.age + 121
  3. Override: age: 21
  4. Result: {id: 1, name: "Sam", age: 21}

Usage Examples

User profile:

function UserProfile() {
  const [user, setUser] = useState({
    name: 'Sam',
    email: '[email protected]',
  })

  function handleAddPhone() {
    setUser({ ...user, phone: '555-1234' })
  }

  function handleUpdateEmail(newEmail) {
    setUser({ ...user, email: newEmail })
  }

  return (
    <div>
      <p>
        {user.name} - {user.email}
      </p>
      <button onClick={handleAddPhone}>Add Phone</button>
      <button onClick={() => handleUpdateEmail('[email protected]')}>
        Update Email
      </button>
    </div>
  )
}

Counter with metadata:

function Counter() {
  const [state, setState] = useState({
    count: 0,
    lastUpdated: null,
  })

  function handleIncrement() {
    setState({
      ...state,
      count: state.count + 1,
      lastUpdated: new Date().toISOString(),
    })
  }

  return (
    <div>
      <p>Count: {state.count}</p>
      <p>Last: {state.lastUpdated}</p>
      <button onClick={handleIncrement}>+1</button>
    </div>
  )
}

Settings panel:

function Settings() {
  const [settings, setSettings] = useState({
    theme: 'light',
    notifications: true,
    fontSize: 16,
  })

  function handleToggleTheme() {
    setSettings({
      ...settings,
      theme: settings.theme === 'light' ? 'dark' : 'light',
    })
  }

  function handleIncreaseFontSize() {
    setSettings({ ...settings, fontSize: settings.fontSize + 2 })
  }

  return (
    <div>
      <p>Theme: {settings.theme}</p>
      <p>Font: {settings.fontSize}px</p>
      <button onClick={handleToggleTheme}>Toggle Theme</button>
      <button onClick={handleIncreaseFontSize}>Increase Font</button>
    </div>
  )
}

Form state:

function LoginForm() {
  const [form, setForm] = useState({
    username: '',
    password: '',
    rememberMe: false,
  })

  function handleUsernameChange(value) {
    setForm({ ...form, username: value })
  }

  function handlePasswordChange(value) {
    setForm({ ...form, password: value })
  }

  function handleToggleRemember() {
    setForm({ ...form, rememberMe: !form.rememberMe })
  }

  return (
    <form>
      <input
        value={form.username}
        onChange={(e) => handleUsernameChange(e.target.value)}
      />
      <input
        type="password"
        value={form.password}
        onChange={(e) => handlePasswordChange(e.target.value)}
      />
      <label>
        <input
          type="checkbox"
          checked={form.rememberMe}
          onChange={handleToggleRemember}
        />
        Remember me
      </label>
    </form>
  )
}

Summary

  • Use spread operator {...obj} to create shallow copy of object
  • Add property: {...obj, newKey: value}
  • Update property: {...obj, existingKey: newValue}
  • Order matters: later properties override earlier ones
  • {...obj, key: value} spreads first, adds/overrides after
  • {key: value, ...obj} may be overridden by spread
  • Increment: {...obj, count: obj.count + 1}
  • Never mutate original: obj.property = value
  • Spread creates new reference → React detects change
  • Pattern: always spread existing properties, then modify
  • Works for any number of property changes
  • Next lesson: removing and nested object updates

Removing Object Properties with Rest Operator

Overview

The rest operator (...) enables removing properties from objects by extracting remaining properties after destructuring specific ones. Essential for immutable state updates in React.

Core Concepts

The Rest Operator

Syntax: Three dots (...) like spread operator, but different usage.

Purpose: Extract remaining properties into new object after picking specific ones.

Pattern: Use with destructuring to separate properties.

Basic example:

const movie = {
  id: 1,
  title: 'Harry Potter',
  year: 2017,
}

const { year, ...rest } = movie
console.log(rest) // {id: 1, title: "Harry Potter"}

Breakdown:

  • year - destructured property
  • ...rest - captures remaining properties
  • rest - new object with id and title only

Variable name: Can name it anything (not just rest).

Rest vs Spread Operator

Similar syntax: Both use ... (three dots).

Different usage:

  • Spread: Copies properties into new object
  • Rest: Extracts remaining properties during destructuring

Spread example:

const copy = { ...original }

Rest example:

const { prop, ...remaining } = original

Removing Properties in React

Pattern: Destructure properties to remove, use rest for remaining.

Example:

import { useState } from 'react'

function App() {
  const [user, setUser] = useState({
    id: 1,
    name: 'Sam',
    age: 20,
    type: 'admin',
  })

  function handleClick() {
    const { id, type, ...rest } = user
    setUser(rest)
  }

  return <button onClick={handleClick}>Remove id and type</button>
}

Result after click:

{
    name: "Sam",
    age: 20
}

How it works:

  1. Destructure properties to remove: id, type
  2. Capture remaining properties: ...rest
  3. rest becomes new object with only name and age
  4. Pass rest to setUser() to update state

Removing Single Property

Example:

const [user, setUser] = useState({
  id: 1,
  name: 'Sam',
  email: '[email protected]',
})

function removeEmail() {
  const { email, ...userWithoutEmail } = user
  setUser(userWithoutEmail)
}

Removing Multiple Properties

Example:

const [product, setProduct] = useState({
  id: 1,
  name: 'Laptop',
  price: 999,
  internalNote: 'Check stock',
  adminOnly: true,
})

function removeInternalData() {
  const { internalNote, adminOnly, ...publicProduct } = product
  setProduct(publicProduct)
}

Use Cases

When to use rest operator:

  • Removing sensitive data before display
  • Filtering object properties
  • Separating internal vs public data
  • Cleaning up temporary properties

Common scenarios:

  • Remove admin fields from user object
  • Strip metadata before sending to API
  • Remove temporary flags after processing
  • Filter out deprecated properties

Simplifying Object Structure

Complex nested structure:

{
    id: 1,
    name: {
        first: "Sam",
        last: "Green"
    },
    age: 30
}

Flattened (simpler):

{
    id: 1,
    firstName: "Sam",
    lastName: "Green",
    age: 30
}

Benefits of flattening:

  • Easier to update immutably
  • Simpler code
  • Less nesting complexity
  • Better performance

When flattening not possible:

  • API returns nested structure you don't control
  • External data source format fixed
  • Third-party library requirements

Alternative for complex structures: Consider Immer library for easier immutable updates.

Usage Examples

Remove admin properties:

function Dashboard() {
  const [user, setUser] = useState({
    id: 1,
    name: 'Sam',
    email: '[email protected]',
    password: 'hashed123',
    role: 'admin',
  })

  function getPublicProfile() {
    const { password, role, ...publicUser } = user
    return publicUser
  }

  return <Profile data={getPublicProfile()} />
}

Remove temporary flags:

function Form() {
  const [formData, setFormData] = useState({
    name: 'Product',
    price: 100,
    isEditing: true,
    hasUnsavedChanges: true,
  })

  function handleSave() {
    const { isEditing, hasUnsavedChanges, ...cleanData } = formData
    // Save only clean data to database
    saveToDatabase(cleanData)
  }

  return <button onClick={handleSave}>Save</button>
}

Remove internal metadata:

function ProductCard() {
  const [product, setProduct] = useState({
    id: 1,
    name: 'Laptop',
    price: 999,
    _internal_id: 'abc123',
    _last_updated: '2024-01-01',
  })

  function displayProduct() {
    const { _internal_id, _last_updated, ...displayData } = product
    return displayData
  }

  return <div>{JSON.stringify(displayProduct())}</div>
}

Event handler with cleanup:

function Settings() {
  const [settings, setSettings] = useState({
    theme: 'dark',
    notifications: true,
    tempPreview: 'light',
    previewMode: true,
  })

  function applySettings() {
    const { tempPreview, previewMode, ...finalSettings } = settings
    setSettings(finalSettings)
  }

  return <button onClick={applySettings}>Apply</button>
}

Multiple removal:

function UserProfile() {
  const [userData, setUserData] = useState({
    id: 1,
    name: 'Sam',
    email: '[email protected]',
    password: 'hash',
    apiKey: 'secret',
    internalNotes: 'VIP',
    createdBy: 'admin',
  })

  function getSafeProfile() {
    const { password, apiKey, internalNotes, createdBy, ...safeData } = userData

    return safeData
  }

  return <Profile user={getSafeProfile()} />
}

Conditional removal:

function DataExport() {
  const [data, setData] = useState({
    id: 1,
    content: 'Data',
    draft: true,
    version: 2,
  })

  function prepareExport(includeDraft) {
    if (includeDraft) {
      return data
    }

    const { draft, version, ...exportData } = data
    return exportData
  }

  return (
    <button onClick={() => console.log(prepareExport(false))}>Export</button>
  )
}

Summary

  • Rest operator: Extract remaining properties into new object
  • Syntax: const {prop1, prop2, ...rest} = object
  • Three dots (...) like spread operator, different usage
  • Use with destructuring: Pick properties to remove, capture rest
  • React pattern: Immutably remove properties from state objects
  • Example:
    const { id, type, ...rest } = user
    setUser(rest) // New state without id and type
    
  • Variable name after ... can be anything (common: rest, remaining, other)
  • Use cases: Remove sensitive data, strip metadata, filter properties, cleanup temporary fields
  • Less common than adding/updating properties
  • Simplification tip: Flatten object structure when possible
  • Avoid deeply nested objects for easier immutable updates
  • Alternative: Immer library for complex nested structures
  • When flattening not possible: API/external data format fixed
  • Maintains immutability (doesn't modify original object)
  • Creates new object with selected properties excluded
  • Essential for proper React state management
  • Can remove single or multiple properties
  • Works with any object structure

Summary

Object Shallow Copy:

  • Use spread operator: {...object}
  • Creates new object with same properties

Adding/Updating Properties:

  • Spread operator adds or updates key/value pairs
  • Order matters with spread operator
  • Pattern: {...state, newKey: newValue}

Avoiding Mutation:

  • Don't modify original state object directly
  • Always create shallow copy first
  • Then add/update properties

Removing Properties:

  • Use rest operator (...) with destructuring
  • Extracts remaining properties after picking specific ones
  • Pattern: const {keyToRemove, ...rest} = object
  • In React: setState(rest) to remove property

Best Practice:

  • Flatten object structure when possible
  • Simplifies code and reduces complexity
  • Easier to manage and update

State with Array of Objects

Overview

Managing state with arrays of objects requires combining array manipulation techniques with immutability principles. Use .map(), .filter(), and .slice() to update state without mutating original data.

Core Concepts

Shallow vs Deep Copy

Shallow copy behavior:

const users = [{ id: 1, name: 'Sam' }]
const shallowCopyOfUsers = [...users]

shallowCopyOfUsers[0].name = 'Alex'
console.log(shallowCopyOfUsers) // [{id: 1, name: "Alex"}]
console.log(users) // [{id: 1, name: "Alex"}]

What happened: Modifying copied array also changed original.

Why: [...users] creates shallow copy:

  • Array itself is copied
  • Objects within are NOT copied
  • Both arrays share references to same objects in memory

Implication: Modifying object through one array affects it in other.

By design: JavaScript language behavior for performance reasons.

Deep Copy (Avoid in React)

Definition: Complete duplicate including all nested objects/arrays.

Method: structuredClone() for deep copying.

Don't use deep copy: Slows down renders significantly.

React approach: Use .map(), .filter(), .slice() instead.

Why React way better: Performance optimized, maintains immutability correctly.

Removing Objects from Array

Pattern: Use .filter() to create new array without specific objects.

Example:

import { useState } from 'react'

function App() {
  const [items, setItems] = useState([
    { id: 1, name: 'Apple' },
    { id: 2, name: 'Carrot' },
    { id: 3, name: 'Banana' },
  ])

  function handleRemoveCarrots() {
    setItems(items.filter((item) => item.name !== 'Carrot'))
  }

  return <button onClick={handleRemoveCarrots}>Remove Carrots</button>
}

How it works:

  • .filter() callback receives each object
  • First call: {id: 1, name: "Apple"}
  • Condition: item.name !== "Carrot"
  • Returns new array without Carrot object

Result:

;[
  { id: 1, name: 'Apple' },
  { id: 3, name: 'Banana' },
]

Original array: Remains unchanged (immutability maintained).

Alternative: Can use .slice() method.

Updating Objects in Array

Pattern: Use .map() to transform specific objects.

Example:

import { useState } from 'react'

function App() {
  const [items, setItems] = useState([
    { id: 1, name: 'Apple' },
    { id: 2, name: 'Carrot' },
    { id: 3, name: 'Banana' },
  ])

  function handleChangeCarrots() {
    setItems((prevItems) =>
      prevItems.map((item) => {
        if (item.name === 'Carrot') {
          return {
            ...item,
            name: 'Peas',
          }
        }
        return item
      }),
    )
  }

  return <button onClick={handleChangeCarrots}>Change Carrots</button>
}

How it works:

  1. .map() iterates through each object
  2. Find target: item.name === "Carrot"
  3. Return new object: {...item, name: "Peas"}
  4. Spread operator creates copy with new name
  5. Other objects returned unchanged

Why spread operator: Creates new object instead of modifying old one.

Why important: Modifying old object modifies object in old state.

Result: Technically still re-renders, but other React features (DevTools, concurrent rendering) might behave unexpectedly without immutability.

Same Techniques as Array State

Apply array methods:

  • .filter() - Remove objects
  • .map() - Update objects
  • .slice() - Extract portions
  • All create new arrays (no mutation)

Key principle: Never mutate original array or objects within it.

Usage Examples

Remove by ID:

function ProductList() {
  const [products, setProducts] = useState([
    { id: 1, name: 'Laptop', price: 999 },
    { id: 2, name: 'Mouse', price: 25 },
    { id: 3, name: 'Keyboard', price: 75 },
  ])

  function removeProduct(productId) {
    setProducts(products.filter((p) => p.id !== productId))
  }

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          {product.name} - ${product.price}
          <button onClick={() => removeProduct(product.id)}>Remove</button>
        </li>
      ))}
    </ul>
  )
}

Update specific property:

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', completed: false },
    { id: 2, text: 'Build project', completed: false },
  ])

  function toggleTodo(id) {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo,
      ),
    )
  }

  return todos.map((todo) => (
    <div key={todo.id}>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => toggleTodo(todo.id)}
      />
      {todo.text}
    </div>
  ))
}

Update multiple properties:

function UserList() {
  const [users, setUsers] = useState([
    { id: 1, name: 'Sam', age: 25, active: true },
    { id: 2, name: 'Alex', age: 30, active: false },
  ])

  function updateUser(id, updates) {
    setUsers(
      users.map((user) => (user.id === id ? { ...user, ...updates } : user)),
    )
  }

  function handleActivate(id) {
    updateUser(id, { active: true, lastLogin: new Date() })
  }

  return users.map((user) => (
    <div key={user.id}>
      {user.name} - {user.active ? 'Active' : 'Inactive'}
      <button onClick={() => handleActivate(user.id)}>Activate</button>
    </div>
  ))
}

Add object to array:

function ShoppingCart() {
  const [cart, setCart] = useState([{ id: 1, name: 'Laptop', quantity: 1 }])

  function addItem(item) {
    setCart([...cart, item])
  }

  function handleAddMouse() {
    addItem({ id: 2, name: 'Mouse', quantity: 1 })
  }

  return <button onClick={handleAddMouse}>Add Mouse</button>
}

Increment quantity:

function Cart() {
  const [items, setItems] = useState([
    { id: 1, name: 'Laptop', quantity: 1 },
    { id: 2, name: 'Mouse', quantity: 2 },
  ])

  function incrementQuantity(id) {
    setItems(
      items.map((item) =>
        item.id === id ? { ...item, quantity: item.quantity + 1 } : item,
      ),
    )
  }

  return items.map((item) => (
    <div key={item.id}>
      {item.name}: {item.quantity}
      <button onClick={() => incrementQuantity(item.id)}>+</button>
    </div>
  ))
}

Remove and update combined:

function TaskManager() {
  const [tasks, setTasks] = useState([
    { id: 1, title: 'Task 1', priority: 'low' },
    { id: 2, title: 'Task 2', priority: 'high' },
  ])

  function deleteTask(id) {
    setTasks(tasks.filter((task) => task.id !== id))
  }

  function setPriority(id, priority) {
    setTasks(
      tasks.map((task) => (task.id === id ? { ...task, priority } : task)),
    )
  }

  return tasks.map((task) => (
    <div key={task.id}>
      {task.title} - {task.priority}
      <button onClick={() => setPriority(task.id, 'high')}>
        High Priority
      </button>
      <button onClick={() => deleteTask(task.id)}>Delete</button>
    </div>
  ))
}

Conditional update:

function ProductInventory() {
  const [products, setProducts] = useState([
    { id: 1, name: 'Laptop', stock: 5, price: 999 },
    { id: 2, name: 'Mouse', stock: 0, price: 25 },
  ])

  function restock(id, amount) {
    setProducts(
      products.map((product) => {
        if (product.id === id && product.stock === 0) {
          return {
            ...product,
            stock: amount,
            price: product.price * 0.9, // 10% discount on restock
          }
        }
        return product
      }),
    )
  }

  return products.map((product) => (
    <div key={product.id}>
      {product.name} - Stock: {product.stock}
      {product.stock === 0 && (
        <button onClick={() => restock(product.id, 10)}>Restock</button>
      )}
    </div>
  ))
}

Summary

  • Arrays of objects: Apply same techniques as State with Arrays chapter
  • Use .map(), .filter(), .slice() to update without mutation
  • Shallow copy: [...array] copies array but NOT objects within
  • Both arrays share references to same objects in memory
  • Modifying object in copy affects original
  • Deep copy: Complete duplicate (avoid - performance issue)
  • Don't use structuredClone() in React
  • JavaScript design: Shallow copy for performance reasons
  • Remove objects: Use .filter() with condition
  • Pattern: items.filter(item => item.id !== targetId)
  • Update objects: Use .map() with conditional return
  • Pattern:
    items.map((item) =>
      item.id === targetId ? { ...item, property: newValue } : item,
    )
    
  • Spread operator critical: Creates new object instead of modifying old one
  • Prevents mutation of objects in old state
  • Why immutability matters: React features and DevTools depend on it
  • Without: Re-renders work but unexpected behavior possible
  • Add objects: [...array, newObject]
  • Original array: Always remains unchanged
  • Alternative methods: .slice() also works for removing
  • Combines array manipulation with object immutability
  • Essential for managing complex state structures
  • Practice needed for comfortable usage

Event Handlers with Parameters

Overview

When handling events for list items, handlers often need parameters (like IDs) to identify which item was acted upon. Use arrow functions in onClick to pass arguments without calling the function immediately.

Core Concepts

The Problem

Rendering list with action buttons:

function App() {
  const [users, setUsers] = useState([
    { id: 1, name: 'Sam' },
    { id: 2, name: 'Alex' },
  ])

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          {user.name} - <button>Delete</button>
        </li>
      ))}
    </ul>
  )
}

Need handleDelete function that receives id:

function handleDelete(id) {
  // Delete user with this id
}

Challenge: How to pass user.id to handleDelete using onClick?

Wrong Approach: Direct Call

// ❌ Wrong - calls function immediately
<button onClick={handleDelete(user.id)}>Delete</button>

Why wrong:

  • handleDelete(user.id) is function call, not function reference
  • Executes immediately during render
  • Return value assigned to onClick (likely undefined)
  • Button click does nothing

Correct: Function Definition

Use arrow function to wrap the call:

// ✅ Correct - function definition
<button onClick={() => handleDelete(user.id)}>Delete</button>

How it works:

  • () => handleDelete(user.id) is function definition
  • React receives function reference
  • On click: React executes arrow function
  • Arrow function calls handleDelete(user.id)
  • Correct parameter passed

Long vs Short Syntax

Long syntax (rarely used):

onClick = {
  function() {
    handleDelete(user.id)
  },
}

Short syntax (standard):

onClick={() => handleDelete(user.id)}

Both equivalent, arrow function preferred.

Complete Example

import { useState } from 'react'

function App() {
  const [users, setUsers] = useState([
    { id: 1, name: 'Sam' },
    { id: 2, name: 'Alex' },
  ])

  function handleDelete(id) {
    setUsers(users.filter((user) => user.id !== id))
  }

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>
          {user.name} -
          <button onClick={() => handleDelete(user.id)}>Delete</button>
        </li>
      ))}
    </ul>
  )
}

Flow:

  1. Click "Delete" next to "Sam"
  2. Arrow function executes
  3. Calls handleDelete(1)
  4. Filters out user with id: 1
  5. Re-render with remaining users

Multiple Parameters

Pass multiple arguments:

function handleUpdate(id, newName) {
  // Update user
}

return <button onClick={() => handleUpdate(user.id, 'NewName')}>Update</button>

Usage Examples

Todo list with remove:

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Buy milk' },
    { id: 2, text: 'Walk dog' },
  ])

  function handleRemove(id) {
    setTodos(todos.filter((todo) => todo.id !== id))
  }

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          {todo.text}
          <button onClick={() => handleRemove(todo.id)}>×</button>
        </li>
      ))}
    </ul>
  )
}

Shopping cart quantity:

function ShoppingCart() {
  const [items, setItems] = useState([
    { id: 1, name: 'Laptop', quantity: 1 },
    { id: 2, name: 'Mouse', quantity: 2 },
  ])

  function handleIncrement(id) {
    setItems(
      items.map((item) =>
        item.id === id ? { ...item, quantity: item.quantity + 1 } : item,
      ),
    )
  }

  function handleDecrement(id) {
    setItems(
      items.map((item) =>
        item.id === id
          ? { ...item, quantity: Math.max(0, item.quantity - 1) }
          : item,
      ),
    )
  }

  return (
    <div>
      {items.map((item) => (
        <div key={item.id}>
          {item.name}: {item.quantity}
          <button onClick={() => handleDecrement(item.id)}>-</button>
          <button onClick={() => handleIncrement(item.id)}>+</button>
        </div>
      ))}
    </div>
  )
}

Toggle item status:

function TaskList() {
  const [tasks, setTasks] = useState([
    { id: 1, text: 'Code review', done: false },
    { id: 2, text: 'Deploy app', done: true },
  ])

  function handleToggle(id) {
    setTasks(
      tasks.map((task) =>
        task.id === id ? { ...task, done: !task.done } : task,
      ),
    )
  }

  return (
    <div>
      {tasks.map((task) => (
        <div key={task.id}>
          <input
            type="checkbox"
            checked={task.done}
            onChange={() => handleToggle(task.id)}
          />
          {task.text}
        </div>
      ))}
    </div>
  )
}

Edit mode with multiple params:

function ProductList() {
  const [products, setProducts] = useState([
    { id: 1, name: 'Phone', price: 599 },
  ])

  function handleUpdatePrice(id, newPrice) {
    setProducts(
      products.map((product) =>
        product.id === id ? { ...product, price: newPrice } : product,
      ),
    )
  }

  return (
    <div>
      {products.map((product) => (
        <div key={product.id}>
          {product.name}: ${product.price}
          <button onClick={() => handleUpdatePrice(product.id, 699)}>
            Increase Price
          </button>
        </div>
      ))}
    </div>
  )
}

Access event object with parameters:

function FormList() {
  const [items, setItems] = useState([{ id: 1, value: '' }])

  function handleChange(id, event) {
    setItems(
      items.map((item) =>
        item.id === id ? { ...item, value: event.target.value } : item,
      ),
    )
  }

  return (
    <div>
      {items.map((item) => (
        <input
          key={item.id}
          value={item.value}
          onChange={(event) => handleChange(item.id, event)}
        />
      ))}
    </div>
  )
}

Summary

  • Problem: Handlers in lists need parameters (IDs, indices, values)
  • Wrong: onClick={handler(arg)} calls immediately, doesn't wait for click
  • Correct: onClick={() => handler(arg)} creates function definition
  • Arrow function wraps actual handler call
  • React executes arrow function on click
  • Arrow function then calls handler with arguments
  • Syntax: onClick={() => functionName(arguments)}
  • Long form: onClick={function() { functionName(args); }}
  • Works with any number of parameters
  • Can pass multiple arguments: () => handler(id, value, status)
  • Combine with event object: (event) => handler(id, event)
  • Essential pattern for interactive lists (delete, edit, toggle)
  • Prevents function execution during render

Summary

Array of Objects in State:

  • Use same techniques as State with Arrays chapter
  • Methods: .map(), .slice(), .filter()
  • Update without mutating original state

Shallow Copy Behavior:

  • Copying array of objects creates shallow copy
  • Array itself is copied, but objects within are NOT
  • Original and copied arrays share references to same objects in memory
  • Modifying nested object affects both arrays

Event Handler with Arguments:

  • Must use function definition: onClick={() => functionName(args)}
  • Function definition prevents immediate execution on render
  • Without arrow function, function runs during render (wrong)

Immer Library Suggestion:

  • For complex nested arrays/objects
  • Allows mutating syntax on draft variable
  • Immutably applies changes to state
  • 14kb package size
  • Keeps original state unchanged

Summary

Progress Summary - Topics Covered:

  • JSX
  • Components
  • StrictMode
  • ReactDOM
  • Props
  • Events
  • Hooks
  • State
  • Rules of React
  • React and Immutability

Milestone: Foundation complete for building React applications with state management


Lifting State Up

Overview

React state is local to the component where it's created. To share state between multiple components, lift the state up to a common ancestor component that can pass it down as props.

Core Concepts

State is Local

State lives in component where useState() called:

function Counter() {
  const [counter, setCounter] = useState(0)
  // counter and setCounter only available here
  return <p>{counter}</p>
}

Problem: Other components can't access counter.

Sharing State Between Components

Scenario: Two sibling components need same state:

function App() {
  return (
    <>
      <Counter />
      <Sidebar />
    </>
  )
}

function Counter() {
  const [counter, setCounter] = useState(0)
  // Has counter state
  return <button>Increment</button>
}

function Sidebar() {
  // Needs counter value but doesn't have it
  return <p>Counter value is ???</p>
}

Visual representation:

<App />
├── <Counter />
│   └── counter state (local to Counter)
└── <Sidebar />
    └── needs counter but can't access it

Solution: Lift State Up

Lift state to common ancestor:

Move useState() from <Counter /> to <App />:

function App() {
  const [counter, setCounter] = useState(0) // Lifted here

  return (
    <>
      <Counter counter={counter} setCounter={setCounter} />
      <Sidebar counter={counter} />
    </>
  )
}

Visual after lifting:

<App />
├── counter state (now in App)
├── <Counter /> receives: counter, setCounter (as props)
└── <Sidebar /> receives: counter (as props)

How It Works

  1. Identify common ancestor: Find first parent of all components needing state
  2. Move state up: Move useState() call to ancestor
  3. Pass down as props: Send state and setter functions as props
  4. Use in children: Components receive and use props

Passing Functions as Props

State setters are functions and can be passed as props:

function App() {
  const [counter, setCounter] = useState(0)

  // Pass setter function down
  return <Counter setCounter={setCounter} />
}

function Counter({ setCounter }) {
  function handleClick() {
    setCounter((prev) => prev + 1)
  }

  return <button onClick={handleClick}>+1</button>
}

Visual Flow

Before lifting (state isolated):

<App />
  ├── <Counter />
  │   ├── counter: 5
  │   └── setCounter
  └── <Sidebar />
      └── ❌ Can't access counter

After lifting (state shared):

<App />
  ├── counter: 5
  ├── setCounter
  ├── <Counter counter={5} setCounter={fn} />
  │   └── ✅ Uses counter prop
  └── <Sidebar counter={5} />
      └── ✅ Uses counter prop

Only Lift What's Needed

Don't lift unnecessarily:

// ✅ Good - local state
function SearchBar() {
  const [searchTerm, setSearchTerm] = useState('')
  // Only used here
  return <input value={searchTerm} />
}

// ❌ Bad - lifted unnecessarily
function App() {
  const [searchTerm, setSearchTerm] = useState('')
  // Only SearchBar needs it
  return <SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
}

Lift only when multiple components need access.

Usage Examples

Counter and display:

function App() {
  const [count, setCount] = useState(0)

  return (
    <>
      <Counter count={count} setCount={setCount} />
      <Display count={count} />
      <AnotherDisplay count={count} />
    </>
  )
}

function Counter({ count, setCount }) {
  return <button onClick={() => setCount(count + 1)}>Increment</button>
}

function Display({ count }) {
  return <p>Count: {count}</p>
}

function AnotherDisplay({ count }) {
  return <p>The value is {count}</p>
}

Form shared between components:

function App() {
  const [username, setUsername] = useState('')

  return (
    <>
      <LoginForm username={username} setUsername={setUsername} />
      <WelcomeMessage username={username} />
    </>
  )
}

function LoginForm({ username, setUsername }) {
  return (
    <input value={username} onChange={(e) => setUsername(e.target.value)} />
  )
}

function WelcomeMessage({ username }) {
  if (!username) return null
  return <p>Welcome, {username}!</p>
}

Theme across app:

function App() {
  const [theme, setTheme] = useState('light')

  return (
    <>
      <Header theme={theme} />
      <Content theme={theme} />
      <Footer theme={theme} setTheme={setTheme} />
    </>
  )
}

function Header({ theme }) {
  return <header className={theme}>Header</header>
}

function Content({ theme }) {
  return <main className={theme}>Content</main>
}

function Footer({ theme, setTheme }) {
  return (
    <footer className={theme}>
      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </footer>
  )
}

Shopping cart and navbar:

function App() {
  const [cartItems, setCartItems] = useState([])

  return (
    <>
      <Navbar cartItems={cartItems} />
      <ProductList cartItems={cartItems} setCartItems={setCartItems} />
      <Cart cartItems={cartItems} setCartItems={setCartItems} />
    </>
  )
}

function Navbar({ cartItems }) {
  return <nav>Cart ({cartItems.length})</nav>
}

function ProductList({ cartItems, setCartItems }) {
  function handleAdd(product) {
    setCartItems([...cartItems, product])
  }

  return <button onClick={() => handleAdd({ id: 1 })}>Add</button>
}

function Cart({ cartItems, setCartItems }) {
  return <div>Cart: {cartItems.length} items</div>
}

Summary

  • React state is local to component where useState() called
  • Other components can't access state directly
  • Lifting state up: Move useState() to common ancestor
  • Common ancestor: first parent component of all components needing state
  • Pass state and setter functions down as props
  • Children receive state via props, not via useState()
  • Functions (like setters) can be passed as props
  • Enables multiple components to share and modify same state
  • Only lift state when multiple components need access
  • Don't lift unnecessarily (keep state as local as possible)
  • Pattern: identify common ancestor → move state → pass as props
  • Next lesson: complete code implementation example

Lifting State Up: Implementation

Overview

Complete code transformation demonstrating how to lift state from child component to common ancestor, enabling state sharing between sibling components.

Core Concepts

Starting Code Structure

Three separate files:

index.jsx (App component):

import Counter from './Counter'
import Sidebar from './Sidebar'

function App() {
  return (
    <>
      <Counter />
      <Sidebar />
    </>
  )
}

Counter.jsx:

import { useState } from 'react'

function Counter() {
  const [counter, setCounter] = useState(0)

  function handleIncrement() {
    setCounter(counter + 1)
  }

  return (
    <>
      <button onClick={handleIncrement}>+ 1</button>
      <p>{counter}</p>
    </>
  )
}

Sidebar.jsx:

function Sidebar() {
  // TODO: receive the `counter` state so we can display it here...
  return <p>The counter value is ...</p>
}

Problem: Sidebar can't access counter state from Counter.

Step 1: Lift State to App

Move useState(0) from Counter to App:

index.jsx:

import { useState } from 'react'
import Counter from './Counter'
import Sidebar from './Sidebar'

function App() {
  const [counter, setCounter] = useState(0) // Moved here

  return (
    <>
      <Counter />
      <Sidebar />
    </>
  )
}

State now in common ancestor above both Counter and Sidebar.

Step 2: Create Handler in App

Move business logic to App:

import { useState } from 'react'
import Counter from './Counter'
import Sidebar from './Sidebar'

function App() {
  const [counter, setCounter] = useState(0)

  function onIncrement() {
    // Handler in App
    setCounter(counter + 1)
  }

  return (
    <>
      <Counter />
      <Sidebar />
    </>
  )
}

Naming convention: on... for handlers passed as props, handle... for internal handlers.

Why on... prefix:

  • Clearly distinguishes prop handlers from internal handlers
  • Signals handler defined outside current component
  • Reminds that logic lives in parent
  • Example: onIncrement (prop) vs handleIncrement (internal)

Step 3: Pass Props to Children

Pass state and handlers down:

import { useState } from 'react'
import Counter from './Counter'
import Sidebar from './Sidebar'

function App() {
  const [counter, setCounter] = useState(0)

  function onIncrement() {
    setCounter(counter + 1)
  }

  return (
    <>
      <Counter counter={counter} onIncrement={onIncrement} />
      <Sidebar counter={counter} />
    </>
  )
}
  • Counter receives: counter and onIncrement
  • Sidebar receives: counter

Step 4: Update Counter Component

Receive and use props:

Counter.jsx (with props object):

function Counter(props) {
  return (
    <>
      <button onClick={props.onIncrement}>+ 1</button>
      <p>{props.counter}</p>
    </>
  )
}

Counter.jsx (with destructuring):

function Counter({ counter, onIncrement }) {
  return (
    <>
      <button onClick={onIncrement}>+ 1</button>
      <p>{counter}</p>
    </>
  )
}

Destructuring advantage: Shorter, no need for props. prefix.

Step 5: Update Sidebar Component

Receive and display counter:

Sidebar.jsx (with props object):

function Sidebar(props) {
  return <p>The counter value is {props.counter}</p>
}

Sidebar.jsx (with destructuring):

function Sidebar({ counter }) {
  return <p>The counter value is {counter}</p>
}

Complete Final Code

index.jsx:

import { useState } from 'react'
import Counter from './Counter'
import Sidebar from './Sidebar'

function App() {
  const [counter, setCounter] = useState(0)

  function onIncrement() {
    setCounter(counter + 1)
  }

  return (
    <>
      <Counter counter={counter} onIncrement={onIncrement} />
      <Sidebar counter={counter} />
    </>
  )
}

Counter.jsx:

function Counter({ counter, onIncrement }) {
  return (
    <>
      <button onClick={onIncrement}>+ 1</button>
      <p>{counter}</p>
    </>
  )
}

Sidebar.jsx:

function Sidebar({ counter }) {
  return <p>The counter value is {counter}</p>
}

Passing Setter Functions Directly

Can pass setCounter instead of wrapper:

function App() {
  const [counter, setCounter] = useState(0)

  return (
    <>
      <Counter counter={counter} setCounter={setCounter} />
      <Sidebar counter={counter} />
    </>
  )
}

function Counter({ counter, setCounter }) {
  function handleIncrement() {
    setCounter(counter + 1)
  }

  return <button onClick={handleIncrement}>+ 1</button>
}

When to use wrapper (onIncrement):

  • Complex logic reused by multiple components
  • Business logic should stay in parent
  • Clearer separation of concerns

When to pass setter directly:

  • Simple state updates
  • Logic specific to child component

Usage Examples

Todo app with sidebar:

function App() {
  const [todos, setTodos] = useState([])

  function onAddTodo(text) {
    setTodos([...todos, { id: Date.now(), text }])
  }

  return (
    <>
      <TodoForm onAddTodo={onAddTodo} />
      <TodoList todos={todos} />
      <TodoSidebar todos={todos} />
    </>
  )
}

function TodoForm({ onAddTodo }) {
  const [input, setInput] = useState('')

  function handleSubmit() {
    onAddTodo(input)
    setInput('')
  }

  return (
    <>
      <input value={input} onChange={(e) => setInput(e.target.value)} />
      <button onClick={handleSubmit}>Add</button>
    </>
  )
}

function TodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  )
}

function TodoSidebar({ todos }) {
  return <p>Total tasks: {todos.length}</p>
}

User profile with multiple displays:

function App() {
  const [user, setUser] = useState({ name: '', age: 0 })

  function onUpdateName(newName) {
    setUser({ ...user, name: newName })
  }

  function onUpdateAge(newAge) {
    setUser({ ...user, age: newAge })
  }

  return (
    <>
      <ProfileForm
        user={user}
        onUpdateName={onUpdateName}
        onUpdateAge={onUpdateAge}
      />
      <ProfileCard user={user} />
      <ProfileSummary user={user} />
    </>
  )
}

function ProfileForm({ user, onUpdateName, onUpdateAge }) {
  return (
    <div>
      <input value={user.name} onChange={(e) => onUpdateName(e.target.value)} />
      <input
        type="number"
        value={user.age}
        onChange={(e) => onUpdateAge(Number(e.target.value))}
      />
    </div>
  )
}

function ProfileCard({ user }) {
  return (
    <div>
      Name: {user.name}, Age: {user.age}
    </div>
  )
}

function ProfileSummary({ user }) {
  return (
    <p>
      {user.name} is {user.age} years old
    </p>
  )
}

Summary

  • Step 1: Move useState() from child to common ancestor
  • Step 2: Create handler functions in ancestor (use on... prefix)
  • Step 3: Pass state and handlers as props to children
  • Step 4: Update children to receive and use props
  • Step 5: Implement destructuring for cleaner syntax
  • Naming convention: on... for prop handlers (external), handle... for internal handlers
  • Can pass setter functions directly or wrap in custom handlers
  • Destructuring props: function Component({prop1, prop2}) vs function Component(props)
  • Both approaches valid, destructuring reduces props. repetition
  • Pattern enables state sharing between sibling components
  • State lives in one place (single source of truth)
  • Multiple components can read and update shared state

Summary

Lifting State Up:

  • State is local to component where it's created
  • Share state between components by lifting to common ancestor
  • Common ancestor passes state and setters as props to children
  • Based on unidirectional data flow (single, predictable direction)
  • State always flows down (parent to child)
  • Makes state changes easier to trace

Implementation Pattern:

  • Create state in parent component
  • Pass state value as prop to children needing it
  • Pass setter functions or custom functions that modify state
  • Use on... naming convention for event handler props
  • Example: onValueChange, onClick, onUpdate

Benefits:

  • Predictable data flow
  • Easier debugging
  • Clear state ownership
  • Simplified component communication

Forms in React

Overview

Forms in React work similarly to HTML forms but require handling the submit event to prevent default browser behavior and access form data using the FormData API.

Core Concepts

Basic Form Setup with useId

Link labels to inputs using useId hook:

import { useId } from 'react'

function UserForm() {
  const id = useId()

  return (
    <form>
      <label htmlFor={id}>Enter your username:</label>
      <input type="text" placeholder="Username" id={id} />

      <input type="submit" value="Go" />
    </form>
  )
}

Why useId: Generates unique ID for each component instance, prevents duplicate IDs when reusing components.

onSubmit Event Handler

Handle form submission on <form> element:

import { useId } from 'react'

function UserForm() {
  const id = useId()

  function handleSubmit(event) {
    event.preventDefault()
    console.log('Form submitted')
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor={id}>Enter your username:</label>
      <input type="text" placeholder="Username" id={id} />

      <input type="submit" value="Go" />
    </form>
  )
}

Form triggers when:

  • User clicks submit button
  • User presses Enter key inside form

event.preventDefault()

Default browser behavior:

  • Form submits data to server
  • Page reloads or navigates to action URL

Why prevent: We want to handle submission in React, not reload page.

function handleSubmit(event) {
  event.preventDefault() // Stops page reload
  // Your logic here
}

Note: React Tutorial automatically prevents default. In your own projects, you must call event.preventDefault() manually.

FormData API

Access form values using JavaScript FormData API:

Step 1: Add name attribute to inputs:

<input type="text" name="username" placeholder="Username" id={id} />

Step 2: Create FormData instance from form:

function handleSubmit(event) {
  event.preventDefault()
  const data = new FormData(event.target) // event.target = form element
  const username = data.get('username')
  console.log(username)
}

How it works:

  • event.target: References the form being submitted
  • new FormData(event.target): Collects all inputs with name attributes
  • data.get("name"): Retrieves value of input with that name

Complete Example

import { useId } from 'react'

function UserForm() {
  const id = useId()

  function handleSubmit(event) {
    event.preventDefault()
    const data = new FormData(event.target)
    const username = data.get('username')
    console.log(username)
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor={id}>Enter your username:</label>
      <input type="text" name="username" placeholder="Username" id={id} />

      <input type="submit" value="Go" />
    </form>
  )
}

Usage Examples

Multi-field form:

function LoginForm() {
  const emailId = useId()
  const passwordId = useId()

  function handleSubmit(event) {
    event.preventDefault()
    const data = new FormData(event.target)
    const email = data.get('email')
    const password = data.get('password')
    console.log({ email, password })
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor={emailId}>Email:</label>
      <input type="email" name="email" id={emailId} />

      <label htmlFor={passwordId}>Password:</label>
      <input type="password" name="password" id={passwordId} />

      <button type="submit">Login</button>
    </form>
  )
}

Form with validation:

function SignupForm() {
  const id = useId()

  function handleSubmit(event) {
    event.preventDefault()
    const data = new FormData(event.target)
    const username = data.get('username')

    if (username.length < 3) {
      alert('Username must be at least 3 characters')
      return
    }

    console.log('Valid username:', username)
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor={id}>Username:</label>
      <input type="text" name="username" id={id} required />
      <button type="submit">Sign Up</button>
    </form>
  )
}

Form with multiple inputs:

function ContactForm() {
  const nameId = useId()
  const emailId = useId()
  const messageId = useId()

  function handleSubmit(event) {
    event.preventDefault()
    const data = new FormData(event.target)

    const formData = {
      name: data.get('name'),
      email: data.get('email'),
      message: data.get('message'),
    }

    console.log('Sending:', formData)
    // Send to API
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor={nameId}>Name:</label>
      <input type="text" name="name" id={nameId} />

      <label htmlFor={emailId}>Email:</label>
      <input type="email" name="email" id={emailId} />

      <label htmlFor={messageId}>Message:</label>
      <textarea name="message" id={messageId} rows={4} />

      <button type="submit">Send</button>
    </form>
  )
}

Form with checkboxes:

function PreferencesForm() {
  const newsletterId = useId()
  const notificationsId = useId()

  function handleSubmit(event) {
    event.preventDefault()
    const data = new FormData(event.target)

    // Checkboxes: checked = "on", unchecked = null
    const preferences = {
      newsletter: data.get('newsletter') === 'on',
      notifications: data.get('notifications') === 'on',
    }

    console.log(preferences)
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor={newsletterId}>
        <input type="checkbox" name="newsletter" id={newsletterId} />
        Subscribe to newsletter
      </label>

      <label htmlFor={notificationsId}>
        <input type="checkbox" name="notifications" id={notificationsId} />
        Enable notifications
      </label>

      <button type="submit">Save</button>
    </form>
  )
}

Form with select:

function OrderForm() {
  const sizeId = useId()

  function handleSubmit(event) {
    event.preventDefault()
    const data = new FormData(event.target)
    const size = data.get('size')
    console.log('Selected size:', size)
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor={sizeId}>Size:</label>
      <select name="size" id={sizeId}>
        <option value="small">Small</option>
        <option value="medium">Medium</option>
        <option value="large">Large</option>
      </select>

      <button type="submit">Order</button>
    </form>
  )
}

Summary

  • Use useId hook to generate unique IDs for label-input associations
  • Add onSubmit handler to <form> element, not submit button
  • Form submits when: submit button clicked OR Enter pressed
  • Always call event.preventDefault() to prevent page reload
  • Give each input unique name attribute
  • Use FormData API: new FormData(event.target)
  • Access values: data.get("inputName")
  • event.target references the form being submitted
  • FormData is JavaScript feature (not React-specific)
  • Checkboxes: checked = "on", unchecked = null
  • Selects and textareas work same as text inputs
  • This approach = uncontrolled inputs (values not in React state)
  • Simpler for basic forms that only need submission
  • Next lesson: controlled inputs for more complex scenarios

Controlled Inputs

Overview

Controlled inputs synchronize form input values with React state, providing real-time access to input values and enabling validation, manipulation, or instant reactions to user input.

Core Concepts

Uncontrolled vs Controlled

Uncontrolled input (previous lesson):

function UserForm() {
  function handleSubmit(event) {
    event.preventDefault()
    const data = new FormData(event.target)
    const username = data.get('username') // Access on submit
    console.log(username)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" name="username" />
      <button type="submit">Submit</button>
    </form>
  )
}

Pros: Simple, less code Cons: Can only read value on submit

Making Input Controlled

Synchronize input with state:

import { useState, useId } from 'react'

function UserForm() {
  const id = useId()
  const [username, setUsername] = useState('')

  function handleSubmit(event) {
    event.preventDefault()
    console.log(username) // Direct access via state
  }

  return (
    <form onSubmit={handleSubmit}>
      <label htmlFor={id}>Enter your username:</label>
      <input
        type="text"
        placeholder="Username"
        id={id}
        value={username}
        onChange={(event) => setUsername(event.target.value)}
      />

      <input type="submit" value="Go" />
    </form>
  )
}

Three key parts:

  1. State: const [username, setUsername] = useState("");
  2. Value prop: value={username} - input displays state value
  3. onChange handler: onChange={e => setUsername(e.target.value)} - updates state when user types

How It Works

Flow:

  1. User types character
  2. onChange fires
  3. setUsername(event.target.value) called
  4. State updates to new value
  5. Re-render with new value={username}
  6. Input displays new value

Always synchronized: username state = input's current value.

event.target.value

What it means:

  • event: The change event object
  • event.target: The input element that fired event
  • event.target.value: Current text in the input

Shortened syntax:

// Long
onChange={event => setUsername(event.target.value)}

// Short (common)
onChange={e => setUsername(e.target.value)}

Both identical, e is standard shorthand for event.

Benefits of Controlled Inputs

Real-time access:

function SearchForm() {
  const [query, setQuery] = useState('')

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <p>Searching for: {query}</p> {/* Live update */}
    </div>
  )
}

Validation:

function UsernameForm() {
  const [username, setUsername] = useState('')
  const isValid = username.length >= 3

  return (
    <div>
      <input value={username} onChange={(e) => setUsername(e.target.value)} />
      {!isValid && <p>Username must be at least 3 characters</p>}
    </div>
  )
}

Manipulation:

function UppercaseForm() {
  const [text, setText] = useState('')

  return (
    <input
      value={text}
      onChange={(e) => setText(e.target.value.toUpperCase())}
    />
  )
}

Common Mistakes

Missing onChange:

// ❌ Error: read-only field
<input type="text" value={username} />

Error message:

You provided a `value` prop to a form field without an `onChange` handler.
This will render a read-only field. If the field should be mutable, use
`defaultValue`. Otherwise, set either `onChange` or `readOnly`.

Fix: Add onChange handler.

Calling setState directly (wrong):

// ❌ Wrong - sets username to event object
<input onChange={setUsername} />

// ✅ Correct - extracts value first
<input onChange={e => setUsername(e.target.value)} />

defaultValue for Uncontrolled

If you want initial value but no control:

// ✅ Uncontrolled with default value
<input type="text" defaultValue="initial-username" />

Difference:

  • value={state}: Controlled (requires onChange)
  • defaultValue="text": Uncontrolled (initial value only)

Tradeoffs

Controlled inputs:

  • ✅ Real-time access to values
  • ✅ Can validate immediately
  • ✅ Can transform input (uppercase, format, etc.)
  • ✅ Can disable submit based on validation
  • ❌ More verbose code
  • ❌ Re-renders on every keystroke

Uncontrolled inputs (FormData):

  • ✅ Simple, less code
  • ✅ Better performance (no re-renders)
  • ❌ Can only access values on submit
  • ❌ No real-time validation

Usage Examples

Multiple controlled inputs:

function LoginForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  function handleSubmit(event) {
    event.preventDefault()
    console.log({ email, password })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />
      <button type="submit">Login</button>
    </form>
  )
}

Character counter:

function CommentForm() {
  const [comment, setComment] = useState('')
  const maxLength = 280

  return (
    <div>
      <textarea
        value={comment}
        onChange={(e) => setComment(e.target.value)}
        maxLength={maxLength}
      />
      <p>
        {comment.length} / {maxLength}
      </p>
    </div>
  )
}

Conditional submit button:

function EmailForm() {
  const [email, setEmail] = useState('')
  const isValidEmail = email.includes('@')

  return (
    <form>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <button type="submit" disabled={!isValidEmail}>
        Submit
      </button>
    </form>
  )
}

Format phone number:

function PhoneForm() {
  const [phone, setPhone] = useState('')

  function handleChange(e) {
    const value = e.target.value.replace(/\D/g, '') // Remove non-digits
    const formatted = value.slice(0, 10) // Max 10 digits
    setPhone(formatted)
  }

  return (
    <input
      type="tel"
      value={phone}
      onChange={handleChange}
      placeholder="1234567890"
    />
  )
}

Live search results:

function SearchBar({ items }) {
  const [query, setQuery] = useState('')
  const results = items.filter((item) =>
    item.toLowerCase().includes(query.toLowerCase()),
  )

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <ul>
        {results.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  )
}

Summary

  • Controlled input: Input synchronized with state variable
  • Requires: state variable + value prop + onChange handler
  • Syntax: value={state} and onChange={e => setState(e.target.value)}
  • e is shorthand for event
  • e.target: The input element
  • e.target.value: Current input value
  • Benefits: Real-time access, validation, manipulation, conditional rendering
  • Drawback: More verbose code, re-renders on every change
  • Missing onChange makes input read-only (warning)
  • Use defaultValue for uncontrolled inputs with initial value
  • When to use controlled: Need validation, formatting, or instant feedback
  • When to use uncontrolled (FormData): Simple forms, only need values on submit
  • Both approaches valid, choose based on requirements

Summary

Form Submission:

  • Use onSubmit event handler on <form> element
  • Call event.preventDefault() to prevent default browser behavior (page reload)
  • FormData API: const data = new FormData(event.target)
  • Give each form element unique name attribute
  • Read values: data.get("name")

Controlled Inputs:

  • Create state variable for input value
  • Set both value and onChange props
  • value prop without onChange makes input read-only
  • Allows real-time validation, manipulation, and reactions
  • More verbose code than uncontrolled inputs

Uncontrolled Inputs:

  • Use defaultValue prop for default initial value
  • No state synchronization required
  • Simpler code

Event Object:

  • React event object differs from native JavaScript event
  • Has target, currentTarget, type properties
  • Access original event: event.nativeEvent
  • React synthetic event for cross-browser compatibility

React Suspense

Overview

Suspense is a built-in React component that displays fallback content (like loaders) while its children are loading asynchronous data or code.

Core Concepts

What is Suspense?

Suspense wraps components that need to load asynchronously:

import { Suspense } from 'react'
import Products from './Products'

function App() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <Products />
    </Suspense>
  )
}

How it works:

  • <Products /> fetches data from API
  • While loading: Shows <p>Loading...</p> (the fallback)
  • After loading: Shows <Products /> with data

Fallback Prop

Fallback: What to display while loading.

// Simple text
<Suspense fallback={<p>Loading...</p>}>
    <Products />
</Suspense>

// Custom component
<Suspense fallback={<Loader />}>
    <Products />
</Suspense>

// Complex UI
<Suspense fallback={<div className="spinner">⏳ Please wait...</div>}>
    <Products />
</Suspense>

Can be any JSX: text, component, styled element.

How Suspense Works Internally

  1. React encounters <Suspense> component
  2. Checks if any child is waiting for promise to complete
  3. If promise pending: Renders fallback
  4. If promise fulfilled: Renders children
  5. If promise rejected: Error boundary handles it (next lesson)

Key insight: Suspense detects promises in children.

Suspense Boundary

<Suspense> also called Suspense boundary:

<Suspense>  ← Boundary
  <ComponentA />
  <ComponentB />
    <ComponentC />  ← If this suspends...
</Suspense>
← React stops here and shows fallback

When any descendant suspends, React stops at nearest <Suspense> and shows its fallback.

Multiple Suspense Boundaries

Can have multiple Suspense components:

function App() {
  return (
    <div>
      <Suspense fallback={<p>Loading products...</p>}>
        <Products />
      </Suspense>

      <Suspense fallback={<p>Loading support...</p>}>
        <Support />
      </Suspense>
    </div>
  )
}

Benefit: Different parts of app show independent loading states.

Use Cases

Primary use: Asynchronous data fetching (covered in fetch chapters).

This chapter: Practice syntax with React.lazy (code splitting).

React.lazy

Defers loading of component until needed:

import React from 'react'

const LazySupport = React.lazy(() => import('./Support'))

function App() {
  return <LazySupport />
}

Why React.lazy?

Without lazy (always loaded):

import Support from './Support' // ❌ Always in bundle

With lazy (loaded on demand):

const LazySupport = React.lazy(() => import('./Support')) // ✅ Only when needed

Benefits:

  • Reduces initial JavaScript bundle size
  • Faster initial page load
  • Code downloaded only when component rendered

Tradeoff:

  • User waits when component first renders
  • Solution: Show loader with Suspense!

React.lazy + Suspense

import React, { Suspense } from 'react'

const LazySupport = React.lazy(() => import('./Support'))

function App() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <LazySupport />
    </Suspense>
  )
}

Flow:

  1. App renders
  2. Encounters <LazySupport />
  3. React dynamically imports ./Support
  4. Suspense shows fallback during import
  5. After import completes, renders <Support />

Shorter Import Syntax

Instead of React.lazy, import lazy directly:

// Long
import React from 'react'
const LazySupport = React.lazy(() => import('./Support'))

// Short (preferred)
import { lazy, Suspense } from 'react'
const LazySupport = lazy(() => import('./Support'))

Note: Use lowercase lazy (it's a function, not component).

lazy() Syntax

const LazyComponent = lazy(() => import('./Component'))

Parts:

  • lazy(): React function for code splitting
  • () => import('./Component'): Arrow function returning dynamic import
  • import('./Component'): Dynamic import (returns promise)
  • Must be arrow function (not direct import)

Usage Examples

Multiple lazy components:

import { lazy, Suspense } from 'react'

const LazyProducts = lazy(() => import('./Products'))
const LazySupport = lazy(() => import('./Support'))
const LazyAbout = lazy(() => import('./About'))

function App() {
  return (
    <>
      <Suspense fallback={<p>Loading products...</p>}>
        <LazyProducts />
      </Suspense>

      <Suspense fallback={<p>Loading support...</p>}>
        <LazySupport />
      </Suspense>

      <Suspense fallback={<p>Loading about...</p>}>
        <LazyAbout />
      </Suspense>
    </>
  )
}

Conditional lazy loading:

import { lazy, Suspense, useState } from 'react'

const LazyAdmin = lazy(() => import('./AdminPanel'))

function App() {
  const [showAdmin, setShowAdmin] = useState(false)

  return (
    <div>
      <button onClick={() => setShowAdmin(true)}>Show Admin Panel</button>

      {showAdmin && (
        <Suspense fallback={<p>Loading admin panel...</p>}>
          <LazyAdmin />
        </Suspense>
      )}
    </div>
  )
}

Nested Suspense:

function App() {
  return (
    <Suspense fallback={<p>Loading app...</p>}>
      <Navbar />

      <Suspense fallback={<p>Loading main content...</p>}>
        <MainContent />
      </Suspense>

      <Footer />
    </Suspense>
  )
}

Custom loader component:

function Spinner() {
  return (
    <div className="spinner-container">
      <div className="spinner" />
      <p>Loading...</p>
    </div>
  )
}

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <Products />
    </Suspense>
  )
}

Route-based code splitting:

import { lazy, Suspense } from 'react'

const HomePage = lazy(() => import('./HomePage'))
const ProductsPage = lazy(() => import('./ProductsPage'))
const AboutPage = lazy(() => import('./AboutPage'))

function App({ currentPage }) {
  return (
    <Suspense fallback={<p>Loading page...</p>}>
      {currentPage === 'home' && <HomePage />}
      {currentPage === 'products' && <ProductsPage />}
      {currentPage === 'about' && <AboutPage />}
    </Suspense>
  )
}

Summary

  • Suspense: Built-in React component for handling loading states
  • Wraps components that load asynchronously (data fetching, code splitting)
  • Syntax: <Suspense fallback={<Loader />}><Component /></Suspense>
  • Shows fallback while any child component suspends
  • Suspense boundary: stops at nearest <Suspense> when child suspends
  • Can have multiple Suspense boundaries for independent loading states
  • React.lazy: Defers component loading until needed (code splitting)
  • Syntax: const LazyComp = lazy(() => import('./Component'))
  • Import lazy from "react" (lowercase, it's a function)
  • Reduces initial bundle size, improves performance
  • Always wrap lazy components with Suspense
  • Main use case: data fetching (covered in later chapters)
  • Practice use case: code splitting with lazy loading
  • Next lesson: Error boundaries for handling failures

Error Boundaries

Overview

Error boundaries catch JavaScript errors in React components and display fallback UI instead of crashing the entire app with a blank screen.

Core Concepts

The Problem

Default behavior: React app error → blank screen.

function BrokenComponent() {
  throw new Error('Something went wrong!')
  return <p>Hello</p>
}

function App() {
  return <BrokenComponent /> // ❌ Blank screen
}

User sees nothing. Bad experience.

The Solution: Error Boundaries

Error boundary wraps components and catches errors:

<ErrorBoundary fallback={<p>An error occurred</p>}>
  <App />
</ErrorBoundary>

If error occurs in <App /> or descendants, shows fallback instead of blank screen.

Why External Package?

React doesn't have hook-based error boundary yet. Built-in solution requires class components (old React syntax).

Solution: Use react-error-boundary package (2.5KB).

Benefits:

  • Small footprint
  • Modern syntax
  • No need to learn classes

Installation

npm install react-error-boundary

Note: Already available in React Tutorial environment.

Basic Usage

Import

import { ErrorBoundary } from 'react-error-boundary'

Wrap Your App

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { ErrorBoundary } from 'react-error-boundary'

function App() {
  return <p>Your app here</p>
}

const root = document.querySelector('#root')
createRoot(root).render(
  <StrictMode>
    <ErrorBoundary fallback={<p>An error has occurred</p>}>
      <App />
    </ErrorBoundary>
  </StrictMode>,
)

What happens:

  • Error in <App /> or any child → Shows <p>An error has occurred</p>
  • No error → Shows <App /> normally

Generic Fallback

Simple text message:

<ErrorBoundary fallback={<p>An error has occurred</p>}>
  <App />
</ErrorBoundary>

Good for: Quick setup, generic error message.

Showing Error Message

Display specific error using FallbackComponent:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { ErrorBoundary } from 'react-error-boundary'

function App() {
  return <p>Your app here</p>
}

function FallbackComponent(props) {
  return (
    <div role="alert">
      <p>An error has occurred:</p>
      <pre>{props.error.message}</pre>
    </div>
  )
}

const root = document.querySelector('#root')
createRoot(root).render(
  <StrictMode>
    <ErrorBoundary FallbackComponent={FallbackComponent}>
      <App />
    </ErrorBoundary>
  </StrictMode>,
)

FallbackComponent receives props:

  • props.error: Error object
  • props.error.message: Error message text

Note: Pass component reference, not JSX: FallbackComponent={FallbackComponent} (not FallbackComponent={<FallbackComponent />})

role="alert"

<div role="alert"> is accessibility feature:

  • Announces error to screen readers
  • Improves experience for users with disabilities
  • Best practice for error messages

Multiple Error Boundaries

Similar to Suspense boundaries, can have multiple:

function App() {
  return (
    <div>
      <ErrorBoundary fallback={<p>Products error</p>}>
        <Products />
      </ErrorBoundary>

      <ErrorBoundary fallback={<p>Support error</p>}>
        <Support />
      </ErrorBoundary>
    </div>
  )
}

Benefit: Error in one section doesn't break entire app.

Best Practice

Always wrap entire app:

createRoot(root).render(
  <StrictMode>
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <App />
    </ErrorBoundary>
  </StrictMode>,
)

Why: Prevents blank screen, always shows something to user.

Usage Examples

With error details:

function ErrorFallback({ error }) {
  return (
    <div role="alert" style={{ padding: 20, background: '#fee' }}>
      <h1>⚠️ Something went wrong</h1>
      <p>Error: {error.message}</p>
      <button onClick={() => window.location.reload()}>Reload Page</button>
    </div>
  )
}

function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <YourApp />
    </ErrorBoundary>
  )
}

Multiple boundaries:

function App() {
  return (
    <div>
      <ErrorBoundary fallback={<p>Header error</p>}>
        <Header />
      </ErrorBoundary>

      <ErrorBoundary fallback={<p>Content error</p>}>
        <MainContent />
      </ErrorBoundary>

      <ErrorBoundary fallback={<p>Sidebar error</p>}>
        <Sidebar />
      </ErrorBoundary>
    </div>
  )
}

With error logging:

function ErrorFallback({ error }) {
  // Log to error tracking service
  console.error('Error caught by boundary:', error)

  return (
    <div role="alert">
      <h2>Oops! Something went wrong</h2>
      <p>We've been notified and are working on a fix.</p>
    </div>
  )
}

Nested boundaries:

// App-level boundary (catch-all)
<ErrorBoundary FallbackComponent={AppErrorFallback}>
  <App />

  {/* Page-level boundary */}
  <ErrorBoundary fallback={<p>Page error</p>}>
    <ProductsPage />

    {/* Component-level boundary */}
    <ErrorBoundary fallback={<p>Widget error</p>}>
      <ExpensiveWidget />
    </ErrorBoundary>
  </ErrorBoundary>
</ErrorBoundary>

Reset functionality:

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>Error: {error.message}</p>
      <button onClick={resetErrorBoundary}>Try again</button>
    </div>
  )
}

// resetErrorBoundary provided by react-error-boundary
;<ErrorBoundary FallbackComponent={ErrorFallback}>
  <App />
</ErrorBoundary>

Development vs production:

function ErrorFallback({ error }) {
  const isDev = process.env.NODE_ENV === 'development'

  return (
    <div role="alert">
      <h1>Application Error</h1>
      {isDev && (
        <details>
          <summary>Error details (dev only)</summary>
          <pre>{error.stack}</pre>
        </details>
      )}
      {!isDev && <p>Please refresh the page or contact support.</p>}
    </div>
  )
}

Summary

  • Error boundary: Catches JavaScript errors in component tree
  • Prevents blank screen, shows fallback UI instead
  • React doesn't have hook-based solution yet (requires classes)
  • Use react-error-boundary package (2.5KB, modern syntax)
  • Install: npm install react-error-boundary
  • Import: import {ErrorBoundary} from "react-error-boundary"
  • Basic syntax: <ErrorBoundary fallback={<p>Error</p>}><App /></ErrorBoundary>
  • With error message: Use FallbackComponent prop, receives props.error
  • props.error.message contains error text
  • Use role="alert" for accessibility
  • Can have multiple error boundaries (like Suspense boundaries)
  • Best practice: Always wrap entire app with error boundary
  • Prevents catastrophic blank screen failures
  • Can nest boundaries for granular error handling
  • Next: Apply Suspense and Error boundaries with data fetching

Summary

Suspense Component:

  • Built-in React component wrapping app section
  • Displays fallback until children finish loading
  • Syntax: <Suspense fallback={<Loader />}>{children}</Suspense>

React.lazy:

  • Function deferring component loading until needed
  • Used with Suspense to show loader during loading
  • Code splitting for performance optimization

Error Boundaries:

  • Catch errors in component tree
  • Use ErrorBoundary from react-error-boundary package
  • Recommended: Wrap whole application
  • Two options:
    • fallback prop - generic error message
    • fallbackComponent prop - specific error details

Error Boundary Limitations:

  • Doesn't catch ALL errors
  • Event handlers: NOT caught
  • Asynchronous code (setTimeout): NOT caught
  • Prevents blank screen on error

Advanced Features:

  • Retry button with resetErrorBoundary
  • Customizable error handling
  • Check documentation for full capabilities
  • Good practice: Skim docs to know possibilities, dive deeper when needed

Summary

Fetch API Basics:

  • Make requests to backend APIs
  • Default method: GET request

Fetch GET Pattern:

fetch(url)
  .then((response) => response.json())
  .then((data) => console.log(data))

Response Object:

  • fetch(url) returns promise resolving to response object
  • Methods: .text(), .json()
  • .json() most common - converts response to JSON
  • .json() returns promise, needs second .then()

Fetcher Function:

fetch(url).then((response) => response.json())
  • Common pattern passed to libraries
  • Library handles rest of processing

Passing Headers:

fetch(url, {
  headers: {
    'Content-Type': 'application/json',
  },
})

POST/PUT/DELETE Requests:

  • Specify method property: fetch(url, {method: "POST"})
  • Send data with body property
  • Convert data: JSON.stringify(data)
  • Pattern:
fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(data),
})

Best Practice:

  • Always console.log(data) to visualize API response
  • Understand data structure before processing

Summary

Fetch API Usage:

  • Make requests to backend APIs
  • Most common pattern: fetch(url).then(response => response.json())
  • Returns promise resolving to JSON object

Fetch Options:

Headers:

  • Second argument: fetch(url, {headers: {...}})
  • Common: "Content-Type": "application/json"

HTTP Methods:

  • Default: GET request
  • Specify method: fetch(url, {method: "POST"}) or "PUT" or "DELETE"

Sending Data:

  • Use body property in options
  • Convert to string: JSON.stringify(data)
  • Pattern:
fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(data),
})

Complete Example:

fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ key: 'value' }),
})
  .then((response) => response.json())
  .then((data) => console.log(data))

Using fetch in React

Overview

React doesn't provide built-in fetch solution. You cannot call fetch() directly in components because it's a side effect. Use libraries like React Query or SWR instead.

Core Concepts

Why Not Direct fetch()?

function App() {
  // ❌ NEVER do this
  fetch('https://api.example.com/data')
    .then((response) => response.json())
    .then((data) => {
      console.log(data)
    })

  return <p>JSX here</p>
}

Problems:

  1. Side effect: Violates React's purity requirement
  2. StrictMode: Renders twice → two fetch calls
  3. Infinite loops: Combining with useState can cause endless fetches
  4. Performance: React can't optimize rendering

React Components Must Be Pure

Pure component: No side effects, can be called multiple times safely.

Side effects include:

  • Network requests (fetch)
  • DOM manipulation
  • Timers (setTimeout, setInterval)
  • Subscriptions

Why purity matters:

  • React pauses/resumes rendering for optimization
  • StrictMode renders twice to detect side effects
  • Re-renders must be predictable

React Specializes in View

React is library for rendering components efficiently, not full framework.

Not included:

  • Data fetching solution
  • Routing (use React Router)
  • State management (though has useState, useContext)

Philosophy: Use ecosystem libraries for non-view concerns.

What About useEffect?

Can you use useEffect for fetch?

Yes, but not ideal:

// Works but not recommended
useEffect(() => {
  fetch(url)
    .then((res) => res.json())
    .then(setData)
}, [])

Problems with useEffect for fetch:

  • Must handle cleanup (component unmounts)
  • Cumbersome cancellation logic
  • Runs after render (slower)
  • No caching
  • No request deduplication

Note: Later chapters cover useEffect in detail.

React 19 and use() Hook

React 19 beta had use() hook for async data:

  • Beta version worked
  • Removed before stable release
  • Official docs mention "working on solution"
  • May take 1+ years to arrive

Current status: No official React fetch solution.

Recommended Libraries

Option 1: SWR

By: Vercel Size: ~11KB Name meaning: Stale-While-Revalidate

import useSWR from 'swr'

function Profile() {
  const { data, error } = useSWR('/api/user', fetcher)

  if (error) return <div>Error</div>
  if (!data) return <div>Loading...</div>
  return <div>Hello {data.name}!</div>
}

Features:

  • Shows cached data immediately
  • Revalidates in background
  • Lightweight
  • Easy to use

Website: swr.vercel.app

Option 2: @tanstack/react-query

By: TanStack Size: 62KB Status: Most popular

import { useQuery } from '@tanstack/react-query'

function Todos() {
  const { data, error, isLoading } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })

  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error</div>
  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  )
}

Features:

  • Powerful caching
  • Request deduplication
  • Background refetching
  • Pagination support
  • Mutation support
  • DevTools

This course uses React Query.

Website: tanstack.com/query

Size Comparison

LibrarySize
SWR11KB
React Query62KB

Choose based on:

  • SWR: Simple projects, size-conscious
  • React Query: Complex apps, advanced features

Error Boundaries

Always wrap fetch components with ErrorBoundary:

import { ErrorBoundary } from 'react-error-boundary'

function App() {
  return (
    <ErrorBoundary fallback={<p>Error loading data</p>}>
      <Products />
    </ErrorBoundary>
  )
}

Why: Network requests can fail, show error instead of blank screen.

Note: Some tutorial examples skip ErrorBoundary for brevity, but use it in production.

React Frameworks

If using framework:

  • Next.js: Server Components, fetch with caching
  • Remix/React Router: Loaders and actions
  • Refer to framework docs

Course coverage: Chapter 60 covers React Server Components with async fetch.

Usage Examples

Why not this:

// ❌ Wrong - side effect in component
function Users() {
  fetch('/api/users')
    .then((res) => res.json())
    .then((users) => console.log(users))

  return <div>Users</div>
}

React Query approach:

// ✅ Correct - using library
import { useQuery } from '@tanstack/react-query'

function Users() {
  const { data: users, isLoading } = useQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then((res) => res.json()),
  })

  if (isLoading) return <div>Loading...</div>
  return <div>{users.length} users</div>
}

SWR approach:

// ✅ Correct - using library
import useSWR from 'swr'

const fetcher = (url) => fetch(url).then((res) => res.json())

function Users() {
  const { data: users, error } = useSWR('/api/users', fetcher)

  if (error) return <div>Error</div>
  if (!users) return <div>Loading...</div>
  return <div>{users.length} users</div>
}

Summary

  • Cannot use fetch() directly in React components (side effect)
  • React renders components multiple times (StrictMode, optimization)
  • Components must be pure, predictable
  • React specializes in view, not data fetching
  • useEffect works but not ideal (cleanup, timing, no caching)
  • React 19 use() hook removed before stable release
  • Solution: Use library (SWR or React Query)
  • SWR: 11KB, simple, Stale-While-Revalidate
  • React Query: 62KB, most popular, powerful features
  • This course uses React Query
  • Always use <ErrorBoundary /> to catch fetch errors
  • React frameworks (Next.js, Remix) have own fetch solutions
  • Next chapters: Install and use React Query

React Query Setup

Overview

React Query requires one-time setup with QueryClientProvider wrapping your app and a QueryClient instance for managing caching and fetching.

Core Concepts

Installation

npm install @tanstack/react-query

Note: Old package name was react-query. Always use @tanstack/react-query for current version.

Setup Requirements

React Query needs:

  1. QueryClientProvider component wrapping app
  2. QueryClient instance passed to provider

Why: Makes fetch functionality available to all child components.

Step 1: Import and Wrap App

Starting code:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'

function App() {
  return <p>your app here</p>
}

createRoot(document.querySelector('#root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

Add QueryClientProvider:

import { StrictMode } from 'react'
import { QueryClientProvider } from '@tanstack/react-query'
import { createRoot } from 'react-dom/client'

function App() {
  return <p>your app here</p>
}

// ⚠️ Incomplete - needs client prop
createRoot(document.querySelector('#root')).render(
  <StrictMode>
    <QueryClientProvider>
      <App />
    </QueryClientProvider>
  </StrictMode>,
)

Status: Incomplete, will error without client prop.

Step 2: Create and Provide QueryClient

Complete setup:

import { StrictMode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createRoot } from 'react-dom/client'

function App() {
  return <p>your app here</p>
}

const queryClient = new QueryClient()

createRoot(document.querySelector('#root')).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </StrictMode>,
)

Key parts:

  1. Import QueryClient and QueryClientProvider
  2. Create instance: const queryClient = new QueryClient()
  3. Pass to provider: client={queryClient}

What is QueryClient?

QueryClient: Central manager for React Query's:

  • Caching
  • Fetching logic
  • Mutation logic
  • Configuration

Think of it as: Configuration object controlling React Query behavior.

One-Time Blueprint

This is standard setup - copy/paste for every project:

// Standard React Query setup
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient()

// In render:
;<QueryClientProvider client={queryClient}>
  <App />
</QueryClientProvider>

Don't overthink: Just use this pattern.

Where to Place Setup

Vite project:

  • Put in main.jsx (entry file)

React Tutorial:

  • Put in index.jsx

General rule: Wherever you call createRoot().render()

Single vs Multiple Clients

Most apps: One client for entire app.

Advanced: Can have multiple providers with different clients:

<QueryClientProvider client={mainClient}>
  <MainApp />

  <QueryClientProvider client={adminClient}>
    <AdminPanel />
  </QueryClientProvider>
</QueryClientProvider>

Typical use case: Rare, stick with one client.

Complete Examples

Minimal setup:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

function App() {
  return <h1>My App</h1>
}

const queryClient = new QueryClient()

createRoot(document.querySelector('#root')).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </StrictMode>,
)

With ErrorBoundary:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'

function App() {
  return <h1>My App</h1>
}

const queryClient = new QueryClient()

createRoot(document.querySelector('#root')).render(
  <StrictMode>
    <ErrorBoundary fallback={<p>An error occurred</p>}>
      <QueryClientProvider client={queryClient}>
        <App />
      </QueryClientProvider>
    </ErrorBoundary>
  </StrictMode>,
)

With Suspense:

import { StrictMode, Suspense } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

function App() {
  return <h1>My App</h1>
}

const queryClient = new QueryClient()

createRoot(document.querySelector('#root')).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <Suspense fallback={<p>Loading...</p>}>
        <App />
      </Suspense>
    </QueryClientProvider>
  </StrictMode>,
)

Custom QueryClient configuration:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000, // 1 minute
      cacheTime: 5 * 60 * 1000, // 5 minutes
    },
  },
})

Vite project (main.jsx):

import React from 'react'
import ReactDOM from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App.jsx'
import './index.css'

const queryClient = new QueryClient()

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>,
)

Common Mistakes

Missing client prop:

// ❌ Error - needs client prop
<QueryClientProvider>
  <App />
</QueryClientProvider>

Creating client inside component:

// ❌ Wrong - creates new client every render
function App() {
  const queryClient = new QueryClient()
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}

// ✅ Correct - create outside
const queryClient = new QueryClient()
function App() {
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}

Forgetting to import:

// ❌ Missing imports
const queryClient = new QueryClient()

// ✅ Must import both
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

Summary

  • Install: npm install @tanstack/react-query
  • Old name: react-query, use @tanstack/react-query instead
  • Setup: One-time configuration at app root
  • Import QueryClient and QueryClientProvider
  • Create instance: const queryClient = new QueryClient()
  • Wrap app: <QueryClientProvider client={queryClient}><App /></QueryClientProvider>
  • QueryClient manages caching, fetching, mutations, configuration
  • Place in main.jsx (Vite) or index.jsx (React Tutorial)
  • Most apps use single client for entire app
  • Can customize QueryClient with options object
  • Don't create client inside components (only once at top level)
  • Next lesson: Make first fetch request with React Query

Fetching Data with React Query

Overview

Use useSuspenseQuery hook to fetch data in React components. It requires queryKey for caching and queryFn for fetching, and automatically suspends component until data loads.

Core Concepts

Basic Fetch Example

Component needing data:

function Users() {
  // TODO: fetch users from API
}

function App() {
  return <Users />
}

Implemented with React Query:

import { useSuspenseQuery } from '@tanstack/react-query'

function Users() {
  const { data } = useSuspenseQuery({
    queryKey: ['users'],
    queryFn: () =>
      fetch('https://react-tutorial-demo.firebaseio.com/users.json').then(
        (response) => response.json(),
      ),
  })

  return <p>There are {data.length} users.</p>
}

API response:

[
  {
    "id": 1,
    "name": "Sam",
    "username": "sam123"
  },
  {
    "id": 2,
    "name": "Alex",
    "username": "alex142"
  }
]

Result: <p>There are 2 users.</p>

useSuspenseQuery Hook

Import:

import { useSuspenseQuery } from '@tanstack/react-query'

Why "Suspense": Compatible with <Suspense> component for loading states.

Required parameters:

  • queryKey: Array for caching identifier
  • queryFn: Function that fetches data

queryKey Parameter

Purpose: Unique identifier for caching this query.

Format: Array with at least one item.

Examples:

// Fetching todos
{
  queryKey: ['todos']
}

// Fetching users
{
  queryKey: ['users']
}

// Fetching products
{
  queryKey: ['products']
}

Naming convention: Make it relevant to endpoint/resource.

What React Query does: Uses key to store fetched data in cache.

queryFn Parameter

Purpose: Function that performs the fetch request.

Format: Must be function definition (not function call).

Correct syntax:

{
  queryFn: () => fetch(url).then((response) => response.json())
}

Why arrow function: React Query calls it at appropriate time.

Common mistake - function call (wrong):

{
  // ❌ WRONG - calls immediately
  queryFn: fetch('/users').then((response) => response.json())
}

Correction - function definition:

{
  // ✅ CORRECT - passes function
  queryFn: () => fetch('/users').then((response) => response.json())
}

Key difference:

  • fetch(url): Executes immediately
  • () => fetch(url): Function React Query will execute

Destructuring the Result

useSuspenseQuery returns object with:

  • data: Fetched data
  • error: Error if request failed
  • isLoading: Loading state (rarely needed with Suspense)
  • Other properties...

Common pattern - destructure data:

const { data } = useSuspenseQuery({
  queryKey: ['users'],
  queryFn: () => fetch(url).then((res) => res.json()),
})

Alternative - two steps:

const response = useSuspenseQuery({
  queryKey: ['users'],
  queryFn: () => fetch(url).then((res) => res.json()),
})
const data = response.data

Both equivalent, destructuring shorter.

Using Data Directly

With useSuspenseQuery: Component suspends until data ready.

const {data} = useSuspenseQuery({...});
return <p>There are {data.length} users.</p>;  // ✅ data always available

Why safe: Component pauses (suspends) during fetch, only renders after data loads.

Different from useQuery: useQuery (not Suspense) requires loading checks:

// useQuery (not covered yet) - needs loading check
const {data, isLoading} = useQuery({...});
if (isLoading) return <p>Loading...</p>;
return <p>{data.length} users</p>;

This course uses useSuspenseQuery (simpler).

Adding Suspense Fallback

Wrap component with <Suspense> to show loader:

import { Suspense } from 'react'
import { useSuspenseQuery } from '@tanstack/react-query'

function Users() {
  const { data } = useSuspenseQuery({
    queryKey: ['users'],
    queryFn: () =>
      fetch('https://react-tutorial-demo.firebaseio.com/users.json').then(
        (response) => response.json(),
      ),
  })

  return <p>There are {data.length} users.</p>
}

function App() {
  return (
    <Suspense fallback={<p>Loading users...</p>}>
      <Users />
    </Suspense>
  )
}

Flow:

  1. App renders
  2. Encounters <Users />
  3. Users calls useSuspenseQuery
  4. Component suspends (pauses)
  5. Suspense shows fallback
  6. After fetch completes, Users renders with data

Usage Examples

Fetching todos:

import { useSuspenseQuery } from '@tanstack/react-query'

function Todos() {
  const { data: todos } = useSuspenseQuery({
    queryKey: ['todos'],
    queryFn: () =>
      fetch('https://jsonplaceholder.typicode.com/todos').then((res) =>
        res.json(),
      ),
  })

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}

Fetching single item:

function Post({ id }) {
  const { data: post } = useSuspenseQuery({
    queryKey: ['post', id],
    queryFn: () =>
      fetch(`https://jsonplaceholder.typicode.com/posts/${id}`).then((res) =>
        res.json(),
      ),
  })

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </article>
  )
}

Multiple queries:

function Dashboard() {
  const { data: users } = useSuspenseQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then((res) => res.json()),
  })

  const { data: posts } = useSuspenseQuery({
    queryKey: ['posts'],
    queryFn: () => fetch('/api/posts').then((res) => res.json()),
  })

  return (
    <div>
      <p>{users.length} users</p>
      <p>{posts.length} posts</p>
    </div>
  )
}

With Suspense and ErrorBoundary:

import { Suspense } from 'react'
import { ErrorBoundary } from 'react-error-boundary'

function App() {
  return (
    <ErrorBoundary fallback={<p>Error loading data</p>}>
      <Suspense fallback={<p>Loading...</p>}>
        <Users />
      </Suspense>
    </ErrorBoundary>
  )
}

Rename data variable:

// Instead of `data`, use descriptive name
const { data: products } = useSuspenseQuery({
  queryKey: ['products'],
  queryFn: () => fetch('/api/products').then((res) => res.json()),
})

return <p>{products.length} products</p>

Summary

  • Import useSuspenseQuery from @tanstack/react-query
  • Compatible with <Suspense> component
  • Requires object with queryKey and queryFn
  • queryKey: Array identifier for caching (e.g., ["users"])
  • Name queryKey relevant to endpoint/resource
  • queryFn: Function definition that fetches data
  • Must be function: () => fetch(url).then(res => res.json())
  • NOT function call: fetch(url)...
  • Returns object with data, error, and other properties
  • Destructure data: const {data} = useSuspenseQuery({...})
  • Can rename: const {data: users} = useSuspenseQuery({...})
  • Component suspends until data ready
  • Safe to use data immediately (no loading checks needed)
  • Wrap with <Suspense> to show loader during fetch
  • Different from useQuery (requires manual loading checks)
  • React Query caches responses automatically
  • Next lesson: Configuration and advanced options

Summary

Fetching in React:

  • Cannot use fetch directly (performs side effect)
  • React specialized in view, doesn't offer fetch solution
  • Use libraries: swr or @tanstack/react-query
  • React Frameworks (Next.js, Remix) have own fetch methods
  • Use <ErrorBoundary /> to catch fetch errors

React Query Setup:

  • One-time project setup required
  • QueryClientProvider wraps entire App
  • Enables React Query in any child component
  • Requires client prop: instance of QueryClient
  • QueryClient: central manager for caching, fetching, mutation logic

useSuspenseQuery Hook:

  • Makes fetch requests in React Query
  • Requires object with queryKey and queryFn
  • queryKey: unique identifier for specific query
  • queryFn: function React Query uses to fetch data
  • Destructure result: const {data} = useSuspenseQuery({...})
  • Use <Suspense> component for loading state

Fetching Data with React Query

Overview

Use useSuspenseQuery hook to fetch data in React components. It requires queryKey for caching and queryFn for fetching, and automatically suspends component until data loads.

Core Concepts

Basic Fetch Example

Component needing data:

function Users() {
  // TODO: fetch users from API
}

function App() {
  return <Users />
}

Implemented with React Query:

import { useSuspenseQuery } from '@tanstack/react-query'

function Users() {
  const { data } = useSuspenseQuery({
    queryKey: ['users'],
    queryFn: () =>
      fetch('https://react-tutorial-demo.firebaseio.com/users.json').then(
        (response) => response.json(),
      ),
  })

  return <p>There are {data.length} users.</p>
}

API response:

[
  {
    "id": 1,
    "name": "Sam",
    "username": "sam123"
  },
  {
    "id": 2,
    "name": "Alex",
    "username": "alex142"
  }
]

Result: <p>There are 2 users.</p>

useSuspenseQuery Hook

Import:

import { useSuspenseQuery } from '@tanstack/react-query'

Why "Suspense": Compatible with <Suspense> component for loading states.

Required parameters:

  • queryKey: Array for caching identifier
  • queryFn: Function that fetches data

queryKey Parameter

Purpose: Unique identifier for caching this query.

Format: Array with at least one item.

Examples:

// Fetching todos
{
  queryKey: ['todos']
}

// Fetching users
{
  queryKey: ['users']
}

// Fetching products
{
  queryKey: ['products']
}

Naming convention: Make it relevant to endpoint/resource.

What React Query does: Uses key to store fetched data in cache.

queryFn Parameter

Purpose: Function that performs the fetch request.

Format: Must be function definition (not function call).

Correct syntax:

{
  queryFn: () => fetch(url).then((response) => response.json())
}

Why arrow function: React Query calls it at appropriate time.

Common mistake - function call (wrong):

{
  // ❌ WRONG - calls immediately
  queryFn: fetch('/users').then((response) => response.json())
}

Correction - function definition:

{
  // ✅ CORRECT - passes function
  queryFn: () => fetch('/users').then((response) => response.json())
}

Key difference:

  • fetch(url): Executes immediately
  • () => fetch(url): Function React Query will execute

Destructuring the Result

useSuspenseQuery returns object with:

  • data: Fetched data
  • error: Error if request failed
  • isLoading: Loading state (rarely needed with Suspense)
  • Other properties...

Common pattern - destructure data:

const { data } = useSuspenseQuery({
  queryKey: ['users'],
  queryFn: () => fetch(url).then((res) => res.json()),
})

Alternative - two steps:

const response = useSuspenseQuery({
  queryKey: ['users'],
  queryFn: () => fetch(url).then((res) => res.json()),
})
const data = response.data

Both equivalent, destructuring shorter.

Using Data Directly

With useSuspenseQuery: Component suspends until data ready.

const {data} = useSuspenseQuery({...});
return <p>There are {data.length} users.</p>;  // ✅ data always available

Why safe: Component pauses (suspends) during fetch, only renders after data loads.

Different from useQuery: useQuery (not Suspense) requires loading checks:

// useQuery (not covered yet) - needs loading check
const {data, isLoading} = useQuery({...});
if (isLoading) return <p>Loading...</p>;
return <p>{data.length} users</p>;

This course uses useSuspenseQuery (simpler).

Adding Suspense Fallback

Wrap component with <Suspense> to show loader:

import {Suspense} from "react";
import {useSuspenseQuery} from "@tantml:function_calls>
<invoke name="create_file">
<parameter name="content"># Refactoring React Query Setup

## Overview

As React Query setup grows complex, refactor by creating `AppSetup` component and moving fetch logic to separate file for cleaner, more maintainable code.

## Core Concepts

### Problem: Complex Render Code

**Before refactoring:**
```jsx
import {StrictMode, Suspense} from "react";
import {createRoot} from "react-dom/client";
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";

function App() {
    return /* ... */;
}

const queryClient = new QueryClient();

createRoot(document.querySelector("#root")).render(
    <StrictMode>
        <QueryClientProvider client={queryClient}>
            <App />
        </QueryClientProvider>
    </StrictMode>
);

Issues:

  • Nested components in render
  • Gets worse with ErrorBoundary, Suspense
  • Hard to read

Solution 1: AppSetup Component

Create wrapper component:

const queryClient = new QueryClient()

function AppSetup() {
  return (
    <StrictMode>
      <QueryClientProvider client={queryClient}>
        <App />
      </QueryClientProvider>
    </StrictMode>
  )
}

Important: queryClient stays outside component (created once).

Simplified render:

createRoot(document.querySelector('#root')).render(<AppSetup />)

Complete Refactored Code

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

function App() {
  return /* ... */
}

const queryClient = new QueryClient()

function AppSetup() {
  return (
    <StrictMode>
      <QueryClientProvider client={queryClient}>
        <App />
      </QueryClientProvider>
    </StrictMode>
  )
}

createRoot(document.querySelector('#root')).render(<AppSetup />)

Benefits:

  • Clean render call
  • Easy to add more wrappers
  • Clear separation

Adding ErrorBoundary

Now simple to add:

function AppSetup() {
  return (
    <StrictMode>
      <ErrorBoundary fallback={<p>An error has occurred.</p>}>
        <QueryClientProvider client={queryClient}>
          <App />
        </QueryClientProvider>
      </ErrorBoundary>
    </StrictMode>
  )
}

Note: Even simple fallback logs errors to console during development.

Solution 2: Refactor Fetch Logic

Problem - repetitive fetch code:

function Users() {
  const { data } = useSuspenseQuery({
    queryKey: ['users'],
    queryFn: () =>
      fetch('https://react-tutorial-demo.firebaseio.com/users.json').then(
        (response) => response.json(),
      ),
  })

  return <p>There are {data.length} users.</p>
}

Issues:

  • Long URL repeated everywhere
  • Fetch logic duplicated
  • Headers/config must be repeated
  • Especially problematic with backends like Supabase (require specific headers)

Create fetcher.jsx File

Define base URL and helper functions:

// fetcher.jsx
const BASE_URL = 'https://react-tutorial-demo.firebaseio.com/'

export function get(endpoint) {
  return fetch(BASE_URL + endpoint).then((response) => response.json())
}

Benefits:

  • Centralized base URL
  • Reusable fetch logic
  • Easy to add headers later
  • DRY (Don't Repeat Yourself)

Use Fetcher in Components

Before:

import { useSuspenseQuery } from '@tanstack/react-query'

function Users() {
  const { data } = useSuspenseQuery({
    queryKey: ['users'],
    queryFn: () =>
      fetch('https://react-tutorial-demo.firebaseio.com/users.json').then(
        (response) => response.json(),
      ),
  })

  return <p>There are {data.length} users.</p>
}

After:

import { useSuspenseQuery } from '@tanstack/react-query'
import { get } from './fetcher.jsx'

function Users() {
  const { data } = useSuspenseQuery({
    queryKey: ['users'],
    queryFn: () => get('users.json'),
  })

  return <p>There are {data.length} users.</p>
}

Much cleaner!

Advanced Fetcher Example

With headers (Supabase example):

// fetcher.jsx
const BASE_URL = 'https://your-project.supabase.co/rest/v1/'
const API_KEY = 'your-api-key'

export function get(endpoint) {
  return fetch(BASE_URL + endpoint, {
    headers: {
      apikey: API_KEY,
      Authorization: `Bearer ${API_KEY}`,
    },
  }).then((response) => response.json())
}

export function post(endpoint, data) {
  return fetch(BASE_URL + endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      apikey: API_KEY,
      Authorization: `Bearer ${API_KEY}`,
    },
    body: JSON.stringify(data),
  }).then((response) => response.json())
}

Component stays simple:

import { get } from './fetcher.jsx'

function Products() {
  const { data } = useSuspenseQuery({
    queryKey: ['products'],
    queryFn: () => get('products'),
  })

  return /* ... */
}

Complete Refactored Example

index.jsx:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ErrorBoundary } from 'react-error-boundary'
import App from './App'

const queryClient = new QueryClient()

function AppSetup() {
  return (
    <StrictMode>
      <ErrorBoundary fallback={<p>An error has occurred.</p>}>
        <QueryClientProvider client={queryClient}>
          <App />
        </QueryClientProvider>
      </ErrorBoundary>
    </StrictMode>
  )
}

createRoot(document.querySelector('#root')).render(<AppSetup />)

fetcher.jsx:

const BASE_URL = 'https://api.example.com/'

export function get(endpoint) {
  return fetch(BASE_URL + endpoint).then((res) => res.json())
}

export function post(endpoint, data) {
  return fetch(BASE_URL + endpoint, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  }).then((res) => res.json())
}

App.jsx:

import { Suspense } from 'react'
import Users from './Users'

function App() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <Users />
    </Suspense>
  )
}

export default App

Users.jsx:

import { useSuspenseQuery } from '@tanstack/react-query'
import { get } from './fetcher'

function Users() {
  const { data: users } = useSuspenseQuery({
    queryKey: ['users'],
    queryFn: () => get('users'),
  })

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

export default Users

Benefits Summary

AppSetup component:

  • Cleaner render code
  • Easy to add wrappers (ErrorBoundary, Suspense, etc.)
  • Better organization

fetcher.jsx file:

  • Centralized fetch logic
  • No URL repetition
  • Easy to add headers/authentication
  • Reusable across components
  • Future-proof (change once, applies everywhere)

Overall:

  • More maintainable
  • Less repetition
  • Easier to test
  • Clearer structure

Summary

  • Complex nested render code → Create AppSetup component
  • AppSetup wraps App with providers (StrictMode, QueryClientProvider, ErrorBoundary)
  • Important: Keep queryClient outside components (create once)
  • Simplified render: createRoot().render(<AppSetup />)
  • Repetitive fetch logic → Create fetcher.jsx file
  • Define BASE_URL and helper functions (get, post, etc.)
  • Import and use: import {get} from "./fetcher"
  • Component code becomes: queryFn: () => get("endpoint")
  • Especially useful with headers/authentication (Supabase, etc.)
  • Benefits: DRY, maintainable, centralized config
  • Next project: Apply these patterns with Supabase backend

React Query Configuration and Caching

Overview

React Query has aggressive default behaviors for keeping data fresh. Understand and customize staleTime, refetchOnWindowFocus, and retry options, plus advanced caching strategies with multi-value queryKeys.

Core Concepts

Renaming Destructured data

Problem: Generic data variable name.

Solution: Rename using JavaScript destructuring:

const {data: users} = useSuspenseQuery({...});

Equivalent to:

const response = useSuspenseQuery({...});
const users = response.data;

Benefits: Clearer code, descriptive variable names.

React Query Aggressive Defaults

React Query's defaults optimize for fresh data but cause frequent refetches.

Default 1: Immediate Staleness

staleTime default: 0 milliseconds.

Meaning: Data considered stale (out of date) immediately after fetch.

Behavior:

  • React Query shows cached data first
  • Refetches in background
  • If data changes, re-renders with new data
  • Does NOT slow down app (cache shown first)
  • MAY cause many fetch requests

When problem: API rate limits, unnecessary server load.

Customize with staleTime:

const { data } = useSuspenseQuery({
  queryKey: ['users'],
  queryFn: () => get('users.json'),
  staleTime: 5 * 60 * 1000, // 5 minutes
})

Effect: Data fresh for 5 minutes, no refetch during that time.

Common values:

  • 0: Immediate staleness (default)
  • 60 * 1000: 1 minute
  • 5 * 60 * 1000: 5 minutes
  • Infinity: Never stale (manual invalidation only)

Default 2: Refetch on Window Focus

refetchOnWindowFocus default: true

Meaning: Refetches when you focus browser tab.

Triggers:

  • Switching from code editor to browser
  • Switching between browser tabs
  • Clicking into browser window

Why: Ensures user sees latest data.

Problem in development: Constant switching → many refetches.

Interaction with staleTime:

  • If data still fresh (within staleTime): No refetch
  • If data stale: Refetches

Disable:

const { data } = useSuspenseQuery({
  queryKey: ['users'],
  queryFn: () => get('users.json'),
  refetchOnWindowFocus: false,
})

When to disable: Data changes infrequently, development annoyance.

Default 3: Automatic Retry

retry default: 3 attempts.

Meaning: Failed queries retry 3 times with exponential backoff.

Backoff: Increasing delay between retries (e.g., 1s, 2s, 4s).

Why: Network hiccups, temporary server issues.

Problem: Slow error display (waits for 3 retries).

Disable:

const { data } = useSuspenseQuery({
  queryKey: ['users'],
  queryFn: () => get('users.json'),
  retry: false,
})

Or customize:

retry: 1,  // Only 1 retry
retry: 5,  // 5 retries

Setting Global Defaults

Problem: Repeating options for every query.

Solution: Configure QueryClient with defaults:

import { QueryClient } from '@tanstack/react-query'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      refetchOnWindowFocus: false,
      retry: false,
    },
  },
})

Effect: All queries use these defaults.

Override per query:

// Uses global defaults
const { data: users } = useSuspenseQuery({
  queryKey: ['users'],
  queryFn: () => get('users'),
})

// Overrides global staleTime
const { data: products } = useSuspenseQuery({
  queryKey: ['products'],
  queryFn: () => get('products'),
  staleTime: 10 * 60 * 1000, // 10 minutes instead
})

queryKey with Multiple Values

Problem: Fetching specific item by ID.

function UserProfile({ id }) {
  const { data } = useSuspenseQuery({
    queryKey: ['users'], // ❌ All users share same cache!
    queryFn: () => get(`users/${id}`),
  })

  return <p>{data.name}</p>
}

Issue: User 1 and User 2 would share cache, showing wrong data.

Solution: Include ID in queryKey:

function UserProfile({ id }) {
  const { data: user } = useSuspenseQuery({
    queryKey: ['users', id], // ✅ Separate cache per ID
    queryFn: () => get(`users/${id}`),
  })

  return <p>{user.name}</p>
}

How it works:

  • ["users", 1]: Cache for user 1
  • ["users", 2]: Cache for user 2
  • Separate cache entries, correct data

Pattern: Include dynamic values in queryKey.

React Query Does More Than Fetch

Features:

  • Caching: Stores responses, avoids redundant requests
  • Deduplication: Two components fetch same data → one request
  • Automatic refetching: Keeps data fresh
  • Background updates: Shows cached data while refetching
  • State management: Manages server state for you
  • Synchronization: Syncs with backend

Example - request deduplication:

function App() {
  return (
    <>
      <ComponentA /> {/* Fetches /users */}
      <ComponentB /> {/* Also needs /users */}
    </>
  )
}

Both use queryKey: ["users"] → React Query makes one request, shares result.

Usage Examples

Global configuration:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,
      refetchOnWindowFocus: false,
      retry: 1,
    },
  },
})

// All queries use these defaults
function Users() {
  const { data: users } = useSuspenseQuery({
    queryKey: ['users'],
    queryFn: () => get('users'),
    // Uses global defaults
  })
  return /* ... */
}

Per-query overrides:

// Aggressive freshness for critical data
const { data: balance } = useSuspenseQuery({
  queryKey: ['balance'],
  queryFn: () => get('account/balance'),
  staleTime: 0, // Always refetch
  refetchOnWindowFocus: true, // Refetch on focus
})

// Relaxed for static data
const { data: categories } = useSuspenseQuery({
  queryKey: ['categories'],
  queryFn: () => get('categories'),
  staleTime: Infinity, // Never stale
  refetchOnWindowFocus: false,
})

Dynamic queryKey:

function Post({ postId }) {
  const { data: post } = useSuspenseQuery({
    queryKey: ['posts', postId],
    queryFn: () => get(`posts/${postId}`),
  })

  return <h1>{post.title}</h1>
}

// Different cache for each post:
// ["posts", 1], ["posts", 2], ["posts", 3]

Multiple dynamic values:

function UserPosts({ userId, category }) {
  const { data: posts } = useSuspenseQuery({
    queryKey: ['posts', userId, category],
    queryFn: () => get(`users/${userId}/posts?category=${category}`),
  })

  return /* ... */
}

// Separate caches:
// ["posts", 1, "tech"], ["posts", 1, "news"], ["posts", 2, "tech"]

Pagination example:

function Products({ page }) {
  const { data: products } = useSuspenseQuery({
    queryKey: ['products', page],
    queryFn: () => get(`products?page=${page}`),
    staleTime: 1 * 60 * 1000, // 1 minute
  })

  return /* ... */
}

// Caches: ["products", 1], ["products", 2], ["products", 3]

Summary

  • Rename data: const {data: users} = useSuspenseQuery({...})
  • React Query defaults are aggressive (optimize for freshness)
  • staleTime: How long data considered fresh (default: 0ms)
  • Set to 5 * 60 * 1000 (5 min) for less aggressive
  • refetchOnWindowFocus: Refetch on tab focus (default: true)
  • Common annoyance in development, set to false
  • retry: Failed queries retry 3x with backoff (default: 3)
  • Set to false or 1 for faster error display
  • Global defaults: Configure in new QueryClient({defaultOptions: {...}})
  • Apply to all queries, can override per query
  • queryKey with dynamic values: Include IDs, page numbers, filters
  • Pattern: ["resource", id], ["posts", postId, category]
  • Ensures separate cache entries for different data
  • React Query provides caching, deduplication, background updates
  • Manages server state and synchronization
  • Next: Mutations (POST, PUT, DELETE requests)

Summary

App Structure:

  • Keep createRoot().render() simple and readable
  • Created AppSetup component wrapping <App />
  • Refactored fetch into separate file to avoid repetition
  • Created get(endpoint) function in fetcher.jsx

JavaScript Destructuring:

  • Can rename destructured properties
  • Pattern: const {oldName: newName} = object

React Query Configuration:

  • Aggressive defaults customizable
  • staleTime: how long data considered fresh
  • refetchOnWindowFocus: refetch when window focused
  • retry: retry failed queries (and how many times)
  • Provide defaults to new QueryClient() call

React Query Caching:

  • Automatically caches API responses
  • Pass multiple values to queryKey for separate cache entries
  • Pattern: queryKey: ['products', userId] vs queryKey: ['products']

Summary

Project X & React Query Mutations concepts:

  • JSX arrays, components, React Query fetching, Suspense integration
  • useMutation hook for POST/PUT/DELETE requests
  • mutation.mutate() execution with dynamic data
  • mutationFn with data parameter, onSuccess callback
  • Supabase API integration with apikey headers, Base URL configuration
  • Login functionality with form data collection, mutation handling
  • Splitting concepts: Forms → Mutations → API integration → Success handling
  • Next: useEffect hook for external system synchronization

React Query Mutations

Overview

Mutations in React Query handle POST, PUT, and DELETE requests that modify server data. Use useMutation hook to define mutations and mutation.mutate() to execute them manually.

Core Concepts

Queries vs Mutations

Queries (GET requests):

  • Fetch data
  • Automatic (React Query manages)
  • Use useSuspenseQuery or useQuery
  • Cached automatically

Mutations (POST/PUT/DELETE):

  • Modify server data (create, update, delete)
  • Manual trigger (you control when)
  • Use useMutation
  • Not cached (single execution)

Basic Fetch Request

Plain fetch POST request:

fetch('https://api.example.com/grades', {
  method: 'POST',
  body: JSON.stringify({
    grade: 15,
  }),
}).then((response) => response.json())

Headers: Optional "Content-Type": "application/json" for better compatibility (some APIs require it).

useMutation Hook

Import:

import { useMutation } from '@tanstack/react-query'

Define mutation:

function App() {
  const mutation = useMutation({
    mutationFn: (data) => {
      return fetch('https://api.example.com/grades', {
        method: 'POST',
        body: JSON.stringify({
          grade: 15, // Hardcoded for now
        }),
      }).then((response) => response.json())
    },
  })

  function handleClick() {
    // TODO: trigger mutation
  }

  return <button onClick={handleClick}>Send grade</button>
}

Parts:

  • useMutation({...}): Define mutation
  • mutationFn: Function that makes fetch request
  • data parameter: Receives data passed to mutate()
  • Return fetch promise

Triggering Mutation

Use mutation.mutate():

function App() {
  const mutation = useMutation({
    mutationFn: (data) => {
      return fetch('https://api.example.com/grades', {
        method: 'POST',
        body: JSON.stringify({
          grade: 15,
        }),
      }).then((response) => response.json())
    },
  })

  function handleClick() {
    mutation.mutate() // Execute fetch request
  }

  return <button onClick={handleClick}>Send grade</button>
}

When called: handleClickmutation.mutate() → fetch request executes.

Passing Dynamic Data

Pass data to mutate():

function App() {
  const mutation = useMutation({
    mutationFn: (data) => {
      return fetch('https://api.example.com/grades', {
        method: 'POST',
        body: JSON.stringify(data), // Use data parameter
      }).then((response) => response.json())
    },
  })

  function handleClick() {
    mutation.mutate({
      grade: 15, // Pass data object
    })
  }

  return <button onClick={handleClick}>Send grade</button>
}

Flow:

  1. Click button
  2. handleClick calls mutation.mutate({grade: 15})
  3. Data object passed to mutationFn as data parameter
  4. Fetch sends JSON.stringify(data)

Result: Flexible, reusable mutations.

onSuccess Callback

Run code after successful mutation:

function App() {
  const mutation = useMutation({
    mutationFn: (data) => {
      return fetch('https://api.example.com/grades', {
        method: 'POST',
        body: JSON.stringify(data),
      }).then((response) => response.json())
    },
    onSuccess: (data) => {
      console.log('Fetch request successful', data)
    },
  })

  function handleClick() {
    mutation.mutate({
      grade: 15,
    })
  }

  return <button onClick={handleClick}>Send grade</button>
}

onSuccess receives: Response data from backend.

Use cases: Show success message, update UI, navigate.

Usage Examples

Create user:

import { useMutation } from '@tanstack/react-query'

function CreateUser() {
  const mutation = useMutation({
    mutationFn: (userData) => {
      return fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData),
      }).then((res) => res.json())
    },
    onSuccess: (newUser) => {
      console.log('Created user:', newUser)
    },
  })

  function handleSubmit(event) {
    event.preventDefault()
    const formData = new FormData(event.target)
    mutation.mutate({
      name: formData.get('name'),
      email: formData.get('email'),
    })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" />
      <input name="email" type="email" placeholder="Email" />
      <button type="submit">Create User</button>
    </form>
  )
}

Update todo:

function TodoItem({ todo }) {
  const mutation = useMutation({
    mutationFn: (updatedTodo) => {
      return fetch(`/api/todos/${todo.id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(updatedTodo),
      }).then((res) => res.json())
    },
  })

  function handleToggle() {
    mutation.mutate({
      ...todo,
      done: !todo.done,
    })
  }

  return (
    <div>
      <input type="checkbox" checked={todo.done} onChange={handleToggle} />
      {todo.text}
    </div>
  )
}

Delete item:

function Product({ product }) {
  const mutation = useMutation({
    mutationFn: (productId) => {
      return fetch(`/api/products/${productId}`, {
        method: 'DELETE',
      }).then((res) => res.json())
    },
    onSuccess: () => {
      console.log('Product deleted')
    },
  })

  function handleDelete() {
    if (confirm('Delete this product?')) {
      mutation.mutate(product.id)
    }
  }

  return (
    <div>
      {product.name}
      <button onClick={handleDelete}>Delete</button>
    </div>
  )
}

With loading state:

function SubmitForm() {
  const mutation = useMutation({
    mutationFn: (formData) => {
      return fetch('/api/submit', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData),
      }).then((res) => res.json())
    },
  })

  function handleSubmit() {
    mutation.mutate({ message: 'Hello' })
  }

  return (
    <div>
      <button onClick={handleSubmit} disabled={mutation.isPending}>
        {mutation.isPending ? 'Submitting...' : 'Submit'}
      </button>
      {mutation.isError && <p>Error: {mutation.error.message}</p>}
      {mutation.isSuccess && <p>Success!</p>}
    </div>
  )
}

With error handling:

function LoginForm() {
  const mutation = useMutation({
    mutationFn: (credentials) => {
      return fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials),
      }).then((res) => {
        if (!res.ok) throw new Error('Login failed')
        return res.json()
      })
    },
    onSuccess: (user) => {
      console.log('Logged in:', user)
    },
    onError: (error) => {
      console.error('Login error:', error)
    },
  })

  function handleLogin() {
    mutation.mutate({
      email: '[email protected]',
      password: 'password123',
    })
  }

  return (
    <div>
      <button onClick={handleLogin}>Login</button>
      {mutation.isError && <p>Login failed</p>}
    </div>
  )
}

Summary

  • Mutations: Handle POST, PUT, DELETE requests (modify server data)
  • Queries: Handle GET requests (fetch data)
  • Import: import {useMutation} from "@tanstack/react-query"
  • Define: const mutation = useMutation({mutationFn: ...})
  • mutationFn: Function that makes fetch request, receives data parameter
  • Execute: mutation.mutate(data) - manual trigger
  • Pass data: mutation.mutate({key: value}) → available as data in mutationFn
  • onSuccess: Callback after successful request, receives response data
  • onError: Callback when request fails, receives error
  • Not cached (unlike queries)
  • Mutations must be triggered manually (not automatic like queries)
  • Access status: mutation.isPending, mutation.isError, mutation.isSuccess
  • Common pattern: Form submissions, create/update/delete operations
  • Use Content-Type header for JSON bodies (backend dependent)
  • Next lesson: Combining mutations with forms

Summary

Mutations in React Query:

  • Fetch requests performing server side effects (update, create, delete records)
  • Use useMutation hook to define mutations

useMutation Hook:

  • Accepts object with mutationFn property
  • mutationFn: function making fetch request
  • Receives data object passed from mutation.mutate()

Triggering Mutations:

  • mutation.mutate({...}): triggers fetch request
  • Passes data to mutationFn

Success Handling:

  • onSuccess property: callback when fetch completes successfully
  • Runs after successful mutation
  • Common for redirects, showing messages, invalidating cache

Summary

Login Project Implementation:

  • Login screen with email/password form
  • callApi(method, endpoint, data) function in fetcher.jsx
  • POST mutation to Supabase API, [email protected] authentication
  • User details returned on success, error messages for failures
  • Content-Type header requirement: "application/json"
  • Components: Form submission → callApi → useMutation → onSuccess callback
  • Next: useEffect hook for side effects and external synchronization
  • Future: Routing for redirects, comprehensive error handling, navigation after login

Introduction to useEffect

Overview

useEffect is a React hook for synchronizing components with outside systems. It runs after rendering and is now considered an "escape hatch" for specific scenarios, not general-purpose logic.

Core Concepts

What is useEffect?

Hook for: Synchronization with external systems (outside React).

Examples of external systems:

  • Analytics services (Google Analytics, Plausible)
  • Browser APIs (localStorage, geolocation)
  • Third-party libraries
  • WebSockets
  • Direct DOM manipulation (rare)

When it runs: After component renders (after JSX in DOM).

React's Changed Guidance

Previously: Common hook used frequently.

Now: Labeled "escape hatch" - use only when necessary.

Use when: Synchronizing with external systems.

Avoid for: Data fetching (use React Query instead), computed values (use regular variables), event handlers.

Analytics Use Case

Scenario: Log page view when component renders.

Services: Google Analytics, Plausible, SimpleAnalytics, etc.

Goal: When <Home /> renders, call logAnalytics("home").

Implementation:

import { useEffect } from 'react'
import { logAnalytics } from './analytics'

function Home() {
  useEffect(() => {
    logAnalytics('home')
  })

  return <h1>Home</h1>
}

Flow:

  1. Component renders
  2. <h1>Home</h1> appears in DOM
  3. After rendering, effect executes
  4. logAnalytics("home") called
  5. Analytics service tracks page view

Note: Code incomplete (covered in next chapters).

Anatomy of useEffect

Import:

import { useEffect } from 'react'

Basic syntax:

useEffect(() => {
  // Effect code here
})

Parts:

  • useEffect(): The hook
  • () => {...}: Function definition (the "effect")
  • Code inside runs after render

Effect definition:

;() => {
  logAnalytics('home')
}

Why function definition: React calls it after render, not during.

Why "After Rendering"?

Rendering phase: React creates JSX, updates DOM.

After rendering: DOM ready, visible to user.

Effect execution: Now safe to interact with external systems.

Order:

  1. React calculates JSX
  2. DOM updates
  3. Browser paints screen
  4. useEffect runs

External System Synchronization

Definition: Keeping component in sync with something outside React.

Examples:

Analytics: Component visible → log view

useEffect(() => {
  logAnalytics('PageName')
})

localStorage: State changes → save to storage

useEffect(() => {
  localStorage.setItem('theme', theme)
})

WebSocket: Component mounts → connect, unmounts → disconnect

useEffect(() => {
  const socket = connect()
  return () => socket.disconnect()
})

Important Notes

Incomplete code: Example above missing dependencies, cleanup (covered next chapters).

Complexity ahead: useEffect has edge cases, best practices, common pitfalls.

Why concise: Foundation only, details in subsequent lessons.

Don't rush: Take time to understand each concept.

Usage Examples

Page view tracking:

import { useEffect } from 'react'

function ProductPage({ productId }) {
  useEffect(() => {
    // Runs after component renders
    trackPageView('product', productId)
  })

  return <div>Product {productId}</div>
}

Document title:

import { useEffect } from 'react'

function Profile({ username }) {
  useEffect(() => {
    // Runs after render
    document.title = `${username}'s Profile`
  })

  return <h1>{username}</h1>
}

Console log (debugging):

import { useEffect } from 'react'

function Counter({ count }) {
  useEffect(() => {
    // Runs after every render
    console.log('Counter rendered with:', count)
  })

  return <p>Count: {count}</p>
}

Focus input (DOM interaction):

import { useEffect, useRef } from 'react'

function SearchBar() {
  const inputRef = useRef()

  useEffect(() => {
    // Runs after render, DOM ready
    inputRef.current.focus()
  })

  return <input ref={inputRef} />
}

Summary

  • useEffect hook for synchronizing with external systems
  • Runs after component renders (after JSX in DOM)
  • Import: import {useEffect} from "react"
  • Syntax: useEffect(() => { /* effect code */ })
  • Pass function definition (React calls after render)
  • Use case: Analytics tracking, browser APIs, third-party libraries
  • React's guidance: "Escape hatch" - use sparingly, specific scenarios only
  • Not for: Data fetching (use React Query), computed values, most logic
  • Order: Render → DOM update → Paint → useEffect executes
  • External system: Anything outside React (analytics, storage, sockets, etc.)
  • Code shown incomplete (dependencies, cleanup covered next lessons)
  • Many edge cases and best practices ahead
  • Don't rush - understand fundamentals first
  • Next lesson: Important gotchas and rules

useEffect Gotchas and StrictMode

Overview

When using useEffect, follow hook rules and understand StrictMode behavior. In development, effects run twice to help catch bugs - this is intentional and beneficial.

Core Concepts

Starting Code Review

Example from previous lesson:

import { useEffect } from 'react'
import { logAnalytics } from './analytics'

function Home() {
  useEffect(() => {
    logAnalytics('home')
  })

  return <h1>Home</h1>
}

What happens: logAnalytics("home") runs after <Home /> renders.

Status: Still incomplete (dependencies covered later).

Rules of Hooks (Reminder)

From Chapter 17 - Two fundamental rules:

Rule 1: Only call Hooks from React functions

  • Call in components
  • Call in custom hooks
  • Never in regular JavaScript functions

Rule 2: Only call Hooks at top level

  • Never inside loops
  • Never inside conditions
  • Never inside nested functions

Examples:

❌ Wrong - conditional hook:

function Component() {
    if (condition) {
        useEffect(() => {...});  // Breaks stable order
    }
    return <div />;
}

✅ Correct - condition inside hook:

function Component() {
  useEffect(() => {
    if (condition) {
      // Condition logic inside effect
    }
  })
  return <div />
}

Why important: React relies on stable hook order to track state/effects.

StrictMode Behavior

What is StrictMode:

  • React wrapper component
  • Helps catch bugs in development
  • No effect in production
  • Mentioned briefly in Chapter 8

Typical setup:

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

Effect: Entire app runs in StrictMode.

StrictMode Development Behavior

In development mode (npm run dev):

Normal React:

  1. Render component
  2. Done

React + StrictMode:

  1. Render component
  2. Remove it from DOM
  3. Render it again

Why? Catches bugs, prevents memory leaks, validates useEffect cleanup.

useEffect Runs Twice Locally

Because of StrictMode double-render:

function Component() {
  useEffect(() => {
    console.log('Effect ran')
  })

  return <div>Hello</div>
}

Console output (development):

Effect ran
Effect ran

Runs twice: Once per render (and StrictMode renders twice).

This is intentional and helpful!

Production vs Development

Development (npm run dev):

  • StrictMode active
  • Components render, unmount, render again
  • Effects run twice
  • Helps catch bugs

Production (deployed app):

  • StrictMode disabled automatically
  • Components render once
  • Effects run once
  • No performance impact

Key point: Double-running only happens locally for debugging.

Don't Disable StrictMode

Temptation: Disable StrictMode to stop double-rendering.

❌ Don't do it:

// Don't remove StrictMode!
createRoot(root).render(<App />) // Missing StrictMode

Why not: Hides bugs that will appear in production.

✅ Embrace it:

  • Accept double-rendering locally
  • Fix revealed bugs
  • Deploy confident, bug-free code

StrictMode benefits:

  • Catches memory leaks
  • Validates effect cleanup
  • Finds impure components
  • Warns about deprecated APIs

What StrictMode Catches

Example problem - missing cleanup:

// ❌ Memory leak (StrictMode reveals it)
function Chat() {
  useEffect(() => {
    const socket = connectToChat()
    // Missing cleanup - socket not closed!
  })
  return <div>Chat</div>
}

StrictMode exposes: Socket connects twice, never disconnects → memory leak.

Fixed version:

// ✅ Proper cleanup
function Chat() {
  useEffect(() => {
    const socket = connectToChat()
    return () => socket.disconnect() // Cleanup
  })
  return <div>Chat</div>
}

StrictMode double-render tests that cleanup works correctly.

Usage Examples

Console logging twice:

function Component() {
  useEffect(() => {
    console.log('Component mounted')
    // Logs twice in development
  })

  return <div>Hello</div>
}

Analytics called twice (temporary in dev):

function Page() {
  useEffect(() => {
    logAnalytics('page-view')
    // Called twice in development
    // Once in production
  })

  return <h1>Page</h1>
}

Condition inside effect (correct):

function Component({ isActive }) {
  useEffect(() => {
    if (isActive) {
      console.log('Active')
    }
  })

  return <div />
}

Hook at top level (correct):

function Component() {
  const [count, setCount] = useState(0) // Top level

  useEffect(() => {
    document.title = `Count: ${count}`
  }) // Top level

  return <button onClick={() => setCount(count + 1)}>Click</button>
}

Wrong - conditional hook:

// ❌ Don't do this
function Component({ shouldTrack }) {
  if (shouldTrack) {
    useEffect(() => {
      track()
    })
  }
  return <div />
}

Right - conditional logic inside:

// ✅ Do this instead
function Component({ shouldTrack }) {
  useEffect(() => {
    if (shouldTrack) {
      track()
    }
  })
  return <div />
}

Summary

  • Rules of Hooks apply to useEffect:
    1. Only call from React functions (components, custom hooks)
    2. Only call at top level (no conditionals, loops, nested functions)
  • Put conditions inside useEffect, not around it
  • StrictMode: Wrapper that helps catch bugs
  • Included by default in new React apps
  • Development behavior: Render → Unmount → Render (double-render)
  • Effect runs twice in development (once per render)
  • Production behavior: Normal single render, effect runs once
  • Double-running is intentional and beneficial
  • Don't disable StrictMode - embrace it
  • Helps catch: Memory leaks, missing cleanup, impure components
  • StrictMode automatically disabled in production builds
  • Accept twice-running locally, fix bugs it reveals
  • Next lessons: Dependencies, cleanup, and complete useEffect usage

Rendering vs Effects

Overview

Side effects cannot run during rendering phase - they must run in event handlers or useEffect. Understanding execution order is critical for proper React component behavior.

Core Concepts

Side Effects Review

Definition (Chapter 21): Change to state or external environment from executing a function.

Examples:

  • Updating state (setState calls)
  • Network requests (fetch)
  • DOM manipulation
  • Logging to console
  • Analytics tracking
  • localStorage access

React requirement: Functions must be pure during rendering.

Where Side Effects Are Allowed

Code structure:

import { useEffect } from 'react'

function App() {
  useEffect(() => {
    // ✅ Side effects allowed
  })

  function handleClick() {
    // ✅ Side effects allowed
  }

  // Rendering phase
  // ❌ Side effects NOT allowed

  return (
    <>
      <p>Hello World</p>
      <button onClick={handleClick}>Click me</button>
    </>
  )
}

Allowed locations:

  1. Event handlers: handleClick() - user-triggered side effects
  2. useEffect: After rendering - automatic side effects

Forbidden location: Rendering phase (component body).

Prefer Events Over Effects

Priority order:

  1. Try event handlers first: User interaction triggers side effects
  2. Use useEffect if needed: When event handlers insufficient

Why useEffect is "escape hatch": Should be last resort, not default choice.

Execution Order

Without StrictMode:

import { useEffect } from 'react'

function App() {
  useEffect(() => {
    console.log('effect')
  })

  function handleClick() {
    console.log('click')
  }

  console.log('rendering')

  return (
    <>
      <p>Hello World</p>
      <button onClick={handleClick}>Click me</button>
    </>
  )
}

Output:

  1. "rendering" - Component body executes
  2. "effect" - useEffect runs after render
  3. "click" - When user clicks button (repeats on each click)

Order: Rendering → Effect → Event handlers (on user input).

With StrictMode (Development)

Output:

  1. "rendering" - First render
  2. "rendering" - Second render (StrictMode double-render)
  3. "effect" - First effect
  4. "effect" - Second effect
  5. "click" - On button click (repeats)

Why: Component added → removed → added again (StrictMode behavior).

Wrong: Side Effect in Rendering

❌ Incorrect:

import { logAnalytics } from './analytics'

function Home() {
  // ❌ WRONG: Side effect in rendering phase
  logAnalytics('home')

  return <h1>Home</h1>
}

Problem: Breaks React purity rules, unpredictable behavior.

Correct: Side Effect in useEffect

✅ Correct:

import { useEffect } from 'react'
import { logAnalytics } from './analytics'

function Home() {
  useEffect(() => {
    // ✅ Correct: Side effect in useEffect
    logAnalytics('home')
  })

  return <h1>Home</h1>
}

Why: Effect runs after render, maintains purity rules.

Visual Flow

Component lifecycle:

  1. Rendering phase: React calls component function, generates JSX
  2. DOM update: JSX committed to DOM
  3. Effect execution: useEffect callbacks run
  4. User interaction: Event handlers fire when triggered

Analytics example execution:

function Home() {
  useEffect(() => {
    logAnalytics('home') // Step 2: Effect runs
  })

  console.log('Rendering phase') // Step 1: Logs first

  return <h1>Home</h1>
}

Output order:

  1. "Rendering phase"
  2. "Pageview: home" (from logAnalytics)

Usage Examples

Analytics tracking (correct):

import { useEffect } from 'react'

function Dashboard() {
  useEffect(() => {
    trackPageView('dashboard') // ✅ After render
  })

  return <div>Dashboard</div>
}

Event handler side effect:

function SaveButton({ data }) {
  function handleSave() {
    // ✅ Side effect in event handler
    localStorage.setItem('data', JSON.stringify(data))
    console.log('Saved')
  }

  return <button onClick={handleSave}>Save</button>
}

Wrong - render phase side effect:

function Counter({ count }) {
  // ❌ Side effect during render
  document.title = `Count: ${count}`

  return <div>{count}</div>
}

Correct - useEffect for document title:

import { useEffect } from 'react'

function Counter({ count }) {
  useEffect(() => {
    // ✅ Side effect after render
    document.title = `Count: ${count}`
  })

  return <div>{count}</div>
}

Logging execution order:

import { useEffect, useState } from 'react'

function ExecutionOrder() {
  const [clicks, setClicks] = useState(0)

  console.log('1. Rendering')

  useEffect(() => {
    console.log('2. Effect ran')
  })

  function handleClick() {
    console.log('3. Button clicked')
    setClicks(clicks + 1)
  }

  return <button onClick={handleClick}>Clicks: {clicks}</button>
}

Initial output: "1. Rendering", "2. Effect ran"

After click: "3. Button clicked", "1. Rendering", "2. Effect ran" (effect re-runs on re-render).

Multiple effects execute in order:

function Component() {
  useEffect(() => {
    console.log('First effect')
  })

  useEffect(() => {
    console.log('Second effect')
  })

  console.log('Rendering')

  return <div />
}

Output: "Rendering", "First effect", "Second effect".

Summary

  • Side effects: Changes to state/external environment (fetch, setState, DOM, logging, etc.)
  • Not allowed: Rendering phase (component body)
  • Allowed: Event handlers, useEffect
  • Prefer events first: Use useEffect only when events insufficient
  • useEffect is "escape hatch" - use sparingly
  • Execution order (no StrictMode): Rendering → Effect → Event handlers (on input)
  • With StrictMode: Rendering → Rendering → Effect → Effect (double-render in dev)
  • Effects run after component renders
  • Event handlers run when user triggers them
  • Wrong pattern: logAnalytics("page") in component body
  • Correct pattern: useEffect(() => { logAnalytics("page"); })
  • Breaking rules causes unpredictable behavior
  • Keep rendering phase pure (no side effects)
  • Multiple useEffects execute in definition order
  • Effects re-run when component re-renders (more on this next chapters)
  • Key rule: If not user-triggered, put side effect in useEffect
  • Next: More useEffect use cases and synchronization patterns

Summary

useEffect Purpose:

  • Synchronize component with outside system
  • Runs after component renders
  • Escape hatch - use sparingly
  • Use case: logging page views to analytics

Rules and Behavior:

  • Follow rules of hooks (stable order)
  • Can have if condition inside useEffect
  • StrictMode renders, removes, renders component (locally)
  • Helps catch bugs - don't disable
  • Production: StrictMode behaves as if doesn't exist
  • useEffect runs twice locally with StrictMode (embrace it)

Side Effects:

  • Changes to state or external environment from function execution
  • NOT allowed in rendering phase
  • Keep side effects in events when possible
  • Otherwise use useEffect

Important Notes:

  • Don't fight StrictMode double-execution
  • Designed to reveal potential issues
  • Production behavior differs from development

useEffect with External Libraries

Overview

useEffect synchronizes React components with third-party libraries. Use it when external code must run after rendering, not during rendering phase.

Core Concepts

Canvas Confetti Library

Library: canvas-confetti - shows celebratory confetti animation 🎉.

Use case: One-time usage in course, so using CDN instead of npm install.

Import from CDN:

import confetti from 'https://esm.sh/canvas-confetti@1'

CDN: esm.sh - serves npm packages with ES module syntax.

Confetti API

Basic usage:

import confetti from 'https://esm.sh/canvas-confetti@1'

confetti({
  spread: 50,
  particleCount: 50,
  origin: { x: 0.5, y: 1 },
  disableForReducedMotion: true,
})

Parameters:

  • spread: Angle of confetti spread
  • particleCount: Number of confetti pieces
  • origin: Start position (x: 0.5 = horizontal center, y: 1 = bottom)
  • disableForReducedMotion: Respects user's motion preference (accessibility)

Synchronizing with React

Problem: How to fire confetti when component renders?

Solution: useEffect - synchronizes React component with external library.

Why useEffect needed:

  • Cannot call confetti in rendering phase (breaks purity rules)
  • Could use event handler, but want automatic execution after render
  • useEffect runs after rendering, perfect timing

Another use case for useEffect: External library synchronization.

Implementation Pattern

Step 1: Import dependencies

import { useEffect } from 'react'
import confetti from 'https://esm.sh/canvas-confetti@1'

Step 2: Setup useEffect

function App() {
  useEffect(() => {
    // Fire confetti here
  })

  return <h1>App</h1>
}

Step 3: Call library function

function App() {
  useEffect(() => {
    confetti({
      spread: 50,
      particleCount: 50,
      origin: { x: 0.5, y: 1 },
      disableForReducedMotion: true,
    })
  })

  return <h1>App</h1>
}

Result: Confetti fires after <h1>App</h1> appears in DOM.

Event vs Effect Choice

Could use event handler:

function App() {
    function handleClick() {
        // ✅ Works: Fire confetti on click
        confetti({...});
    }

    return <button onClick={handleClick}>Celebrate!</button>;
}

Or use effect:

function App() {
    useEffect(() => {
        // ✅ Works: Fire confetti after render
        confetti({...});
    });

    return <h1>Welcome!</h1>;
}

Choose based on when you want it:

  • User-triggered: Event handler (click, submit, etc.)
  • Automatic after render: useEffect

Note on Completeness

Current code: Slightly incomplete.

Why: Dependencies not covered yet (next lessons).

What's missing: Dependency array for useEffect (controls when effect re-runs).

Still valid: Code works, but will learn refinements ahead.

Usage Examples

Confetti on page load:

import { useEffect } from 'react'
import confetti from 'https://esm.sh/canvas-confetti@1'

function SuccessPage() {
  useEffect(() => {
    confetti({
      spread: 70,
      particleCount: 100,
      origin: { x: 0.5, y: 0.5 },
      disableForReducedMotion: true,
    })
  })

  return <h1>Success! 🎉</h1>
}

Confetti with button (event handler):

import confetti from 'https://esm.sh/canvas-confetti@1'

function CelebrateButton() {
  function handleClick() {
    confetti({
      spread: 50,
      particleCount: 50,
      origin: { x: 0.5, y: 1 },
      disableForReducedMotion: true,
    })
  }

  return <button onClick={handleClick}>Celebrate 🎉</button>
}

Multiple confetti bursts:

import { useEffect } from 'react'
import confetti from 'https://esm.sh/canvas-confetti@1'

function Victory() {
  useEffect(() => {
    // First burst
    confetti({
      spread: 50,
      particleCount: 50,
      origin: { x: 0.3, y: 0.8 },
    })

    // Second burst (delayed)
    setTimeout(() => {
      confetti({
        spread: 50,
        particleCount: 50,
        origin: { x: 0.7, y: 0.8 },
      })
    }, 200)
  })

  return <h1>You Won!</h1>
}

Other external library examples:

Chart library:

import { useEffect } from 'react'
import Chart from 'https://esm.sh/chart.js'

function SalesChart({ data }) {
  useEffect(() => {
    const chart = new Chart(ctx, {
      type: 'bar',
      data: data,
    })
  })

  return <canvas id="chart"></canvas>
}

Animation library:

import { useEffect } from 'react'
import anime from 'https://esm.sh/animejs'

function AnimatedBox() {
  useEffect(() => {
    anime({
      targets: '.box',
      translateX: 250,
      rotate: '1turn',
    })
  })

  return <div className="box">Animated</div>
}

Third-party SDK:

import { useEffect } from 'react'

function AnalyticsDashboard() {
  useEffect(() => {
    // Initialize external analytics SDK
    window.analytics.init('API_KEY')
    window.analytics.track('Dashboard Viewed')
  })

  return <div>Dashboard</div>
}

Summary

  • Use case: Synchronize React component with third-party library
  • canvas-confetti: Library for confetti animations
  • CDN import: import confetti from "https://esm.sh/canvas-confetti@1"
  • esm.sh: CDN serving npm packages with ES module syntax
  • API: confetti({spread, particleCount, origin, disableForReducedMotion})
  • origin: {x: 0.5, y: 1} = horizontal center, bottom of screen
  • disableForReducedMotion: Accessibility feature, respects user preferences
  • Cannot call in rendering phase: Breaks React purity rules
  • Two options: Event handler (user-triggered) or useEffect (automatic)
  • useEffect runs after rendering, ideal timing for external libraries
  • Another useEffect use case: External library synchronization (in addition to analytics)
  • Pattern: Import library → useEffect → call library function
  • Code shown slightly incomplete (dependencies covered next chapters)
  • Choose wisely: Event if user-triggered, useEffect if automatic after render
  • Key principle: Synchronization means keeping component in sync with external system
  • Similar pattern works for: Charts, animations, SDKs, any external JavaScript library
  • Next: Behavior when component re-renders (state updates)

useEffect and State Updates

Overview

Effects in useEffect re-run when components re-render (state changes). Understanding this behavior is critical to avoid infinite loops and unexpected side effects.

Core Concepts

Effect Behavior on Re-render

Question: What happens to effects when component re-renders?

Answer: They run again.

Why: React maintains synchronization with external systems on every render.

State Causes Re-renders

Reminder: Calling setState triggers component re-render.

Example with state:

import { useEffect, useState } from 'react'
import confetti from 'https://esm.sh/canvas-confetti@1'

function App() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    confetti({
      spread: 50,
      particleCount: 50,
      origin: { x: 0.5, y: 1 },
      disableForReducedMotion: true,
    })
  })

  function handleClick() {
    setCount(count + 1)
  }

  return (
    <>
      <h1>App</h1>
      <button onClick={handleClick}>Click me</button>
    </>
  )
}

What happens when button clicked:

  1. handleClick executes
  2. setCount(count + 1) called
  3. State updates
  4. Component re-renders
  5. useEffect runs again
  6. Confetti fires again

Result: Confetti every click (probably not intended).

Synchronization Behavior

React's goal: Maintain synchronization with external systems.

On every render: React assumes external state might be stale, re-synchronizes.

With confetti: Every re-render fires confetti (unintended).

Analytics Example

Same problem:

import { useEffect, useState } from 'react'
import { logAnalytics } from './analytics'

function Home() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    logAnalytics('home')
  })

  function handleClick() {
    setCount(count + 1)
  }

  return (
    <>
      <h1>Home</h1>
      <button onClick={handleClick}>Click me</button>
    </>
  )
}

Behavior:

  • Initial render: logAnalytics("home") called once
  • Each button click: logAnalytics("home") called again
  • Problem: Analytics logged repeatedly, not once per page view

Not intended: We want analytics once, not on every re-render.

Fix coming: Next chapter covers dependency array to control when effects run.

Infinite Loop Danger

Critical warning: setState in useEffect often causes infinite loops.

Example:

function Home() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    // ❌ DANGER: Infinite loop
    setCount(count + 1)
  })

  return <h1>Home</h1>
}

Why infinite loop:

  1. Component renders
  2. useEffect runs → setCount(count + 1)
  3. State changes
  4. Component re-renders (back to step 2)
  5. Loop repeats forever

Flow:

Render → Effect (setState) → Re-render → Effect (setState) → Re-render → ...

Protection: React Tutorial has safeguards that stop suspected infinite loops.

Real apps: Browser may freeze or crash without protection.

When setState in useEffect Is Valid

Valid scenarios (rare):

  • Conditional setState (with dependency array)
  • setState based on external data fetch
  • One-time initialization

Example (preview - incomplete):

useEffect(() => {
  if (condition) {
    setState(value) // ✅ May be valid with dependencies
  }
}, [dependencies]) // Dependency array prevents infinite loop

Key: Dependency array (covered next chapter) controls when effect runs.

Purpose of This Lesson

Goal: Visualize behavior before learning the fix.

Current focus: Understand effects re-run on re-renders.

Next lessons: Dependency arrays, controlling effect execution.

Don't worry: Incomplete code is intentional, fixes coming soon.

Usage Examples

Confetti on every render (unintended):

import { useEffect, useState } from 'react'
import confetti from 'https://esm.sh/canvas-confetti@1'

function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    confetti({ spread: 50, particleCount: 30 })
    // ❌ Fires every time count changes
  })

  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}

Analytics on every render (unintended):

import { useEffect, useState } from 'react'

function Dashboard() {
  const [view, setView] = useState('overview')

  useEffect(() => {
    trackEvent('dashboard_view')
    // ❌ Tracks every re-render, not just view changes
  })

  return (
    <div>
      <button onClick={() => setView('overview')}>Overview</button>
      <button onClick={() => setView('details')}>Details</button>
    </div>
  )
}

Infinite loop example:

import { useEffect, useState } from 'react'

function InfiniteLoop() {
  const [data, setData] = useState([])

  useEffect(() => {
    // ❌ INFINITE LOOP
    setData([...data, 'item'])
  })

  return <div>{data.length} items</div>
}

Flow: Render → Effect adds item → Re-render → Effect adds item → Forever.

Multiple state updates (multiple re-renders):

import { useEffect, useState } from 'react'

function MultiState() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('')

  useEffect(() => {
    console.log('Effect ran')
    // Logs on every count OR name change
  })

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <input value={name} onChange={(e) => setName(e.target.value)} />
    </div>
  )
}

Every keystroke: setName → re-render → effect runs.

Valid setState in effect (with dependencies - preview):

import { useEffect, useState } from 'react'

function ValidExample() {
  const [data, setData] = useState(null)

  useEffect(() => {
    fetch('/api/data')
      .then((res) => res.json())
      .then(setData)
    // ✅ Valid: runs once (with proper dependencies)
  }, []) // Empty array = run once

  return <div>{data ? data.value : 'Loading...'}</div>
}

Key: Empty dependency array [] = run once only (explained next chapter).

Summary

  • Effects re-run: When component re-renders
  • Re-render trigger: State updates (setState calls)
  • Synchronization: React re-synchronizes with external systems on every render
  • Confetti example: Fires every button click (every re-render)
  • Analytics example: Logs every re-render (not intended behavior)
  • Problem: Effects run too often without control
  • Fix coming: Dependency array (next chapter) controls when effects run
  • Infinite loop warning: setState in useEffect often causes infinite loops
  • Infinite loop flow: Render → Effect (setState) → Re-render → Effect → Forever
  • Protection: React Tutorial safeguards stop infinite loops
  • Real danger: Browser freeze/crash without protection
  • Avoid: Unconditional setState in useEffect
  • Valid setState: Rare cases with conditions and dependency arrays
  • Lesson goal: Visualize behavior before learning solution
  • Current code: Intentionally incomplete (dependencies covered next)
  • Key concept: React tries to maintain synchronization on every render
  • Effects react to any state change, not specific ones (without dependencies)
  • Next: Dependency arrays, running effects once, conditional execution
  • Don't use setState in useEffect until learning dependency arrays

Summary

useEffect with Third-Party Libraries:

  • Synchronize React component with external libraries
  • Cannot run in rendering phase
  • Run in event or useEffect

Re-render Behavior:

  • Component re-renders → effects in useEffect run again
  • Can cause repeated synchronization (analytics hits, confetti displays)
  • May not match requirements (once-only effects)

Common Issue:

  • Calling setState inside useEffect can cause infinite loop
  • Pattern: render → effect setState → re-render → effect setState → loop

Incomplete Synchronization:

  • Effect runs more often than needed without control
  • Need dependencies to control when effect re-runs
  • Will learn cleanup and dependencies in next chapters

useEffect Dependencies

Overview

Dependencies control when effects re-synchronize. Use empty array [] for "run once" effects, or specify reactive values to re-run when those values change.

Core Concepts

Synchronization Problem

Previous issue: Effect runs on every re-render.

Example:

import { useEffect, useState } from 'react'
import confetti from 'https://esm.sh/canvas-confetti@1'

function App() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    confetti()
  })

  function handleClick() {
    setCount(count + 1)
  }

  return (
    <>
      <p>Count is {count}</p>
      <button onClick={handleClick}>Click me</button>
    </>
  )
}

Problem: Confetti fires every button click (every re-render).

Cause: No dependencies specified → synchronization happens too often.

useEffect Second Argument

Function signature:

useEffect(callbackFn, dependencies)

Parameters:

  1. callbackFn: Effect function (what to run)
  2. dependencies: Array of reactive values (when to run)

Default: undefined (no dependencies specified).

Previous code: We omitted second argument → defaults to undefined.

To control re-synchronization: Pass array of dependencies.

Empty Dependency Array

Syntax: [] (empty array).

Meaning: Effect does NOT depend on anything that might change.

Result: Effect runs once (after initial render), never re-synchronizes.

Important: []undefined (omitting argument).

Before (runs on every re-render):

useEffect(() => {
  confetti()
})

After (runs once only):

useEffect(() => {
  confetti()
}, [])

Effect: Adding [] instructs React: "Don't re-synchronize this effect."

Empty Array Behavior

When effect runs:

  1. After component renders (first time)
  2. Never again (no re-synchronization)

State updates: Don't trigger effect re-run.

Example:

import { useEffect, useState } from 'react'
import confetti from 'https://esm.sh/canvas-confetti@1'

function App() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    confetti({
      spread: 50,
      particleCount: 50,
      origin: { x: 0.5, y: 1 },
      disableForReducedMotion: true,
    })
  }, []) // Empty dependency

  function handleClick() {
    setCount(count + 1)
  }

  return <button onClick={handleClick}>Add 1 to {count}</button>
}

Flow:

  1. Component renders
  2. Effect runs (confetti fires)
  3. User clicks button
  4. State updates (count: 1)
  5. Component re-renders
  6. Effect does NOT run (no re-synchronization)

Result: Confetti fires once only, not on every click.

StrictMode with Empty Dependencies

Reminder: StrictMode double-renders in development.

With empty dependency []:

  1. Component renders
  2. Effect runs
  3. Component removed (StrictMode)
  4. Cleanup runs (if provided)
  5. Component renders again
  6. Effect runs again

In development: Effect still runs twice (due to mount-unmount-mount).

In production: Effect runs once only.

This is expected: Empty array doesn't prevent StrictMode behavior.

What Are Dependencies?

Definition (brief): Reactive values used in effect that should be declared as dependencies.

Reactive value: Value that can change (state, props, etc.).

Full explanation: Next lesson (this is simplified introduction).

When to specify dependencies: When effect uses values that might change AND you want to re-run effect when they change.

Visualization

Code flow with empty dependency:

function App() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        confetti({...});
    }, []);  // ← Empty dependency

    function handleClick() {
        setCount(count + 1);
    }

    return <button onClick={handleClick}>Add 1 to {count}</button>;
}

Execution:

  1. Initial render → count: 0
  2. Effect runs → confetti fires
  3. Button click → count: 1
  4. Re-render → JSX updates
  5. Effect does NOT run → no confetti

Key difference from before: State changes don't re-trigger effect.

Usage Examples

Confetti once (fixed):

import { useEffect, useState } from 'react'
import confetti from 'https://esm.sh/canvas-confetti@1'

function Success() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    confetti({ spread: 50, particleCount: 50 })
  }, []) // Runs once

  return (
    <div>
      <h1>Success!</h1>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
    </div>
  )
}

Analytics once per page:

import { useEffect } from 'react'

function Dashboard() {
  useEffect(() => {
    trackPageView('dashboard')
  }, []) // Track once when page loads

  return <div>Dashboard</div>
}

Initialize third-party library:

import { useEffect } from 'react'

function Chart() {
  useEffect(() => {
    const chart = initializeChart()
    // Chart initialized once
  }, []) // Run once only

  return <canvas id="chart"></canvas>
}

Wrong - no dependency (runs every render):

function Component() {
  const [data, setData] = useState([])

  useEffect(() => {
    fetch('/api/data')
      .then((res) => res.json())
      .then(setData)
    // ❌ Fetches on every re-render (infinite loop potential)
  }) // Missing []

  return <div>{data.length} items</div>
}

Correct - empty dependency:

function Component() {
  const [data, setData] = useState([])

  useEffect(() => {
    fetch('/api/data')
      .then((res) => res.json())
      .then(setData)
  }, []) // ✅ Fetches once only

  return <div>{data.length} items</div>
}

Setup on mount:

import { useEffect } from 'react'

function App() {
  useEffect(() => {
    console.log('App mounted')
    document.title = 'My App'
  }, []) // Runs once when app loads

  return <div>App</div>
}

Event listener (incomplete - needs cleanup):

import { useEffect } from 'react'

function WindowTracker() {
  useEffect(() => {
    function handleResize() {
      console.log('Window resized')
    }
    window.addEventListener('resize', handleResize)
    // TODO: cleanup needed (next lessons)
  }, []) // Setup once

  return <div>Tracking window size</div>
}

Summary

  • useEffect second argument: Dependencies array
  • Signature: useEffect(callbackFn, dependencies)
  • Dependencies: Array of reactive values effect depends on
  • React re-runs effect when dependencies change
  • Empty dependency []: Effect does NOT need to re-synchronize
  • [] means: "Run once after initial render, never again"
  • []undefined (different behaviors)
  • Without []: Effect runs on every re-render
  • With []: Effect runs once only (ignores subsequent re-renders)
  • Reactive value: Value that can change (state, props, etc.)
  • Full dependency explanation in next lesson
  • StrictMode behavior: Effect still runs twice in development (mount-unmount-mount)
  • Empty array doesn't prevent StrictMode double-run
  • Production: Effect runs once with []
  • Use case: One-time setup (analytics, confetti, initialization)
  • Fixes problem of effects running too often
  • State updates don't re-trigger effects with []
  • When to use []: Effect should run once and never re-synchronize
  • Coming next: Non-empty dependencies, reactive values, when to re-synchronize
  • Current knowledge: Work in progress (more details ahead)

useEffect Dependencies (Continued)

Overview

Specify reactive values (state, props) in dependency array to control when effects re-synchronize. Effect re-runs only when specified dependencies change, not on every re-render.

Core Concepts

Dependency Array Review

From previous lesson: useEffect accepts dependencies argument.

Purpose: Array of dependencies specifying when effect must re-synchronize.

Empty array []: Never re-synchronize (run once only).

This lesson: Non-empty arrays with specific dependencies.

Effect with Count Dependency

Example:

import { useState, useEffect } from 'react'

function App() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    console.log('Synchronizing with count')
  }, [count])

  function handleClick() {
    setCount(count + 1)
  }

  return <button onClick={handleClick}>Add 1</button>
}

Behavior:

  1. Component renders → Effect runs → Logs "Synchronizing with count"
  2. Button click → count changes → Effect re-runs → Logs again
  3. Every click → count changes → Effect re-runs

Key point: Effect re-runs only when count changes.

Multiple State Variables

Example with two states:

import { useState, useEffect } from 'react'

function App() {
  const [count, setCount] = useState(0)
  const [isEnabled, setIsEnabled] = useState(true)

  useEffect(() => {
    console.log('Synchronizing with count', count)
  }, [count])

  return (
    <>
      <button onClick={() => setCount(count + 1)}>Add 1</button>
      <button onClick={() => setIsEnabled(!isEnabled)}>Toggle</button>
    </>
  )
}

Behavior:

  • Click "Add 1" → count changes → Effect re-runs → Logs
  • Click "Toggle" → isEnabled changes → Effect does NOT re-run

Why: Effect dependencies: [count] (only depends on count).

Effect ignores: isEnabled changes (not in dependency array).

Multiple Dependencies

To depend on both values:

useEffect(() => {
  console.log('Synchronizing', count, isEnabled)
}, [count, isEnabled])

Now effect re-runs when:

  • count changes
  • isEnabled changes
  • Either value changes

Reactive Values

Definition: Value that can change on re-renders.

Examples:

  • State variables (useState)
  • Props
  • Context values
  • Derived values from above

Non-reactive values:

  • Regular JavaScript variables (outside component)
  • Constants defined outside component

Rule: If effect uses reactive value, declare it as dependency.

Props as Dependencies

Component receiving prop:

import { useEffect } from 'react'

function Counter({ counter }) {
  useEffect(() => {
    console.log(counter)
  }, [counter]) // Prop as dependency

  return <p>Counter is {counter}</p>
}

When prop changes: Effect re-synchronizes.

Usage:

function App() {
  const [value, setValue] = useState(0)

  return (
    <>
      <Counter counter={value} />
      <button onClick={() => setValue(value + 1)}>Increment</button>
    </>
  )
}

Flow:

  1. Button click → value updates
  2. Counter receives new counter prop
  3. Prop change triggers effect re-run
  4. Console logs new counter value

ESLint and Dependencies

Linter role: Warns about missing dependencies.

Setup: From Chapter 4 (Vite + ESLint setup).

What ESLint checks: Every reactive value used in effect is in dependency array.

Important: Linter doesn't run on React Tutorial (run locally to see warnings).

Example - no dependencies (linter silent):

useEffect(() => {
  console.log(counter)
}) // No warning: effect runs on every render

Linter logic: No dependency array → runs every render → counter always fresh.

Example - empty array (linter warns):

useEffect(() => {
  console.log(counter)
}, []) // ⚠️ Warning: missing dependency 'counter'

ESLint message:

React Hook useEffect has a missing dependency: 'counter'.
Either include it or remove the dependency array.

Linter warns when:

  • You specify dependencies ([] or [other])
  • But forget reactive values used in effect

Correct version:

useEffect(() => {
  console.log(counter)
}, [counter]) // ✅ No warning

Work in Progress Note

Status: Still learning useEffect (not complete).

One main topic remaining: Explained in next chapter.

For now: Practice dependency arrays with examples.

Usage Examples

Single dependency:

function Timer({ seconds }) {
  useEffect(() => {
    document.title = `Timer: ${seconds}s`
  }, [seconds]) // Re-run when seconds changes

  return <p>{seconds} seconds</p>
}

Multiple dependencies:

function StatusDisplay({ isOnline, username }) {
  useEffect(() => {
    console.log(`${username} is ${isOnline ? 'online' : 'offline'}`)
  }, [isOnline, username]) // Re-run when either changes

  return (
    <p>
      {username}: {isOnline ? '🟢' : '🔴'}
    </p>
  )
}

State as dependency:

function Tracker() {
  const [page, setPage] = useState('home')

  useEffect(() => {
    trackPageView(page)
  }, [page]) // Track when page changes

  return (
    <div>
      <button onClick={() => setPage('home')}>Home</button>
      <button onClick={() => setPage('about')}>About</button>
    </div>
  )
}

Derived value (doesn't need to be dependency):

function Component({ value }) {
  const doubled = value * 2 // Derived from value

  useEffect(() => {
    console.log(doubled)
  }, [value]) // Depend on value, not doubled

  return <p>{doubled}</p>
}

Why: doubled changes when value changes, no need for both.

Multiple states, selective dependency:

function Form() {
  const [name, setName] = useState('')
  const [email, setEmail] = useState('')

  useEffect(() => {
    // Only validate name
    if (name.length < 3) {
      console.log('Name too short')
    }
  }, [name]) // Only re-run on name change

  return (
    <>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
    </>
  )
}

Typing email doesn't trigger effect (not in dependencies).

Props with state:

function Display({ initialValue }) {
  const [count, setCount] = useState(0)

  useEffect(() => {
    console.log('Total:', initialValue + count)
  }, [initialValue, count]) // Both prop and state

  return (
    <button onClick={() => setCount(count + 1)}>{initialValue + count}</button>
  )
}

Empty vs specific dependencies:

function Comparison() {
  const [clicks, setClicks] = useState(0)

  // Runs once only
  useEffect(() => {
    console.log('Component mounted')
  }, [])

  // Runs on every clicks change
  useEffect(() => {
    console.log('Clicks:', clicks)
  }, [clicks])

  return <button onClick={() => setClicks(clicks + 1)}>Click</button>
}

Summary

  • Dependencies: Array of reactive values specifying when to re-synchronize
  • Reactive values: State, props, context - values that can change on re-renders
  • Effect runs after initial render AND when dependencies change
  • Selective re-runs: Effect only re-runs when specified dependencies change
  • Multiple states: Effect ignores changes to states not in dependency array
  • Multiple dependencies: [state1, state2] - re-run when either changes
  • Props as dependencies: [propName] - re-run when prop changes
  • ESLint checks: Warns about missing dependencies in dependency array
  • Linter silent when no dependency array (runs every render, so always fresh)
  • ESLint warns when: Dependencies specified but reactive value missing
  • Warning message: "React Hook useEffect has a missing dependency"
  • Correct pattern: Every reactive value used in effect → in dependency array
  • Run code locally with ESLint to catch dependency errors
  • React Tutorial doesn't show ESLint warnings (local setup required)
  • Derived values: Don't need to be dependencies if source value is included
  • Work in progress: One more main topic coming next chapter
  • Key rule: If effect uses reactive value, declare as dependency
  • Empty array [] vs specific dependencies [count] vs no array (undefined)
  • Each has different re-synchronization behavior
  • Practice needed: Next challenges will reinforce concepts

Summary

useEffect Dependencies:

  • Second argument: array of reactive values
  • Reactive values: can change on re-renders (state, props)
  • Effect runs after render and when dependencies change
  • Empty array []: effect does NOT need to re-synchronize
  • Means: run after initial render, don't run again

Dependency Array Differences:

  • [] (empty): never re-synchronize
  • undefined or skipped: different behavior than []
  • ESLint warns about incorrect dependency lists

When Effect Runs:

  • After component renders (always)
  • Every time dependencies change
  • Specify dependencies to control re-synchronization timing
  • Prevents unnecessary re-runs

useEffect Cleanup

Overview

Effects may require cleanup functions to prevent memory leaks. Return a function from useEffect to tell React how to stop the effect. StrictMode reveals memory leaks by double-rendering components.

Core Concepts

Why Cleanup Is Needed

So far: Simple effects without cleanup.

Problem: Some effects create side effects that must be stopped.

Without cleanup causes:

  • Memory leaks
  • Unnecessary side effects continuing
  • Multiple timers/listeners/requests stacking up

React's perspective: Effects are synchronization - must know how to start AND stop.

Timer Example (Memory Leak)

Goal: Basic timer incrementing every second.

Implementation:

import { useEffect, useState } from 'react'

function Timer() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    // ⚠️ Incomplete code: has memory leak
    setInterval(() => {
      setCount((c) => c + 1)
    }, 1_000)
  }, [])

  return <p>Count is: {count}</p>
}

Why setInterval in useEffect: setInterval is side effect, cannot run in rendering phase.

The problem: Memory leak exists.

Why memory leak: React can start/stop effects anytime. If React stops effect before interval executes, pending interval remains for removed component.

Consequences: Errors, increased memory usage, unexpected behavior.

Memory Leaks Are Hard to Find

Challenge: Not obvious where memory leaks occur.

Good news: StrictMode makes them visible.

StrictMode purpose: Double-rendering reveals memory issues.

StrictMode Reveals the Problem

StrictMode behavior (development):

  1. Render component (mount)
  2. Remove component (unmount)
  3. Render component again (mount)

Effect behavior:

  1. Effect starts (first render)
  2. Effect stops (component removed)
  3. Effect starts again (second render)

With timer and no cleanup:

  • First render → interval scheduled
  • Component removed → interval NOT cancelled (no cleanup)
  • Second render → another interval scheduled
  • Result: Two intervals running (count increments by 2, not 1)

Symptom: Timer increments by 2 instead of 1 (unexpected behavior).

StrictMode success: Memory leak immediately visible.

Cleanup Function Solution

Fix: Tell React how to stop the effect.

Method: Return function definition from useEffect.

Corrected code:

import { useEffect, useState } from 'react'

function Timer() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount((c) => c + 1)
    }, 1_000)

    // ✅ Cleanup function
    return () => {
      clearInterval(intervalId)
    }
  }, [])

  return <p>Count is: {count}</p>
}

Changes made:

  1. Save interval ID: const intervalId = setInterval(...)
  2. Return cleanup function: return () => { clearInterval(intervalId); }

How setInterval/clearInterval Work

setInterval: Schedules repeated execution, returns timer ID.

clearInterval: Cancels timer using timer ID.

Pattern:

const id = setInterval(callback, delay)
// Later...
clearInterval(id) // Cancel timer

In React: Save ID, pass to clearInterval in cleanup.

Cleanup Function Execution

When cleanup runs:

  1. Component unmounts (removed from DOM)
  2. Before effect re-runs (re-synchronization)

Purpose: Stop previous effect before starting new one.

StrictMode Flow with Cleanup

Complete flow:

  1. <Timer /> rendered (mounted)
  2. Effect starts → interval scheduled (every 1,000ms)
  3. React removes component (unmount) → cleanup runs → interval cancelled
  4. <Timer /> rendered again (mounted)
  5. Effect starts → interval scheduled (every 1,000ms)

Result: Only one interval running (correct behavior).

Without cleanup: Two intervals (increments by 2).

With cleanup: One interval (increments by 1).

Why StrictMode Is Important

Purpose: Catch memory leaks during development.

How: Double-render exposes cleanup issues.

If you skip cleanup:

  • Two setInterval calls active
  • Timer increases at double pace
  • Obvious indicator to check for memory leak

Production note: StrictMode disabled in production (no double-render).

React's Internal Features

Cleanup used for: Concurrent rendering and other React features under the hood.

Why important: React needs ability to start/stop effects at any time.

Best practice: Always provide cleanup for effects that create side effects.

Usage Examples

Timer with cleanup (correct):

import { useEffect, useState } from 'react'

function Countdown({ start }) {
  const [time, setTime] = useState(start)

  useEffect(() => {
    const id = setInterval(() => {
      setTime((t) => t - 1)
    }, 1_000)

    return () => clearInterval(id) // ✅ Cleanup
  }, [])

  return <p>Time: {time}s</p>
}

Multiple intervals (each needs cleanup):

function DoubleTimer() {
  const [count1, setCount1] = useState(0)
  const [count2, setCount2] = useState(0)

  useEffect(() => {
    const id1 = setInterval(() => setCount1((c) => c + 1), 1_000)
    const id2 = setInterval(() => setCount2((c) => c + 1), 2_000)

    return () => {
      clearInterval(id1) // ✅ Clean both
      clearInterval(id2)
    }
  }, [])

  return (
    <div>
      {count1} - {count2}
    </div>
  )
}

setTimeout with cleanup:

function DelayedMessage() {
  const [show, setShow] = useState(false)

  useEffect(() => {
    const id = setTimeout(() => {
      setShow(true)
    }, 3_000)

    return () => clearTimeout(id) // ✅ Cancel timeout
  }, [])

  return show ? <p>Hello!</p> : <p>Wait...</p>
}

Why cleanup needed: If component unmounts before 3 seconds, prevent setState on unmounted component.

Animation with cleanup:

function AnimatedBox() {
  const [position, setPosition] = useState(0)

  useEffect(() => {
    const id = setInterval(() => {
      setPosition((p) => p + 1)
    }, 50)

    return () => clearInterval(id) // ✅ Stop animation
  }, [])

  return <div style={{ left: position }}>Box</div>
}

Wrong - no cleanup (memory leak):

function LeakyTimer() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    // ❌ Memory leak: no cleanup
    setInterval(() => {
      setCount((c) => c + 1)
    }, 1_000)
  }, [])

  return <p>{count}</p>
}

In StrictMode: Two intervals run, count jumps by 2.

Real-world timer with pause:

function PausableTimer() {
  const [count, setCount] = useState(0)
  const [isPaused, setIsPaused] = useState(false)

  useEffect(() => {
    if (isPaused) return

    const id = setInterval(() => {
      setCount((c) => c + 1)
    }, 1_000)

    return () => clearInterval(id) // ✅ Cleanup
  }, [isPaused]) // Re-run when pause state changes

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setIsPaused(!isPaused)}>
        {isPaused ? 'Resume' : 'Pause'}
      </button>
    </div>
  )
}

Summary

  • Some effects require cleanup to prevent memory leaks
  • Cleanup function: Function returned from useEffect
  • Tells React how to stop the effect
  • Memory leak causes: Multiple timers, listeners, requests stacking without cleanup
  • Effects = synchronization: must define start AND stop
  • setInterval: Returns timer ID for cancellation
  • clearInterval: Cancels timer using ID
  • Pattern: Save ID, return cleanup function that calls clearInterval
  • Cleanup runs when:
    1. Component unmounts (removed from DOM)
    2. Before effect re-runs (re-synchronization)
  • StrictMode reveals memory leaks: Double-render exposes cleanup issues
  • StrictMode flow: Mount → Unmount (cleanup runs) → Mount
  • Without cleanup: Multiple effects active (e.g., timer increments by 2)
  • With cleanup: Previous effect stopped before new one starts
  • StrictMode intentionally designed to catch these bugs
  • Don't disable StrictMode - embrace it for catching issues
  • Cleanup function syntax: return () => { /* cleanup code */ }
  • Used in React's concurrent rendering features
  • React may start/stop effects at any time (not just unmount)
  • Memory leaks hard to find - StrictMode makes them obvious
  • Production: StrictMode disabled, effects run normally
  • Best practice: Always provide cleanup for side-effect-creating effects
  • Next lesson: More cleanup examples (event listeners, etc.)
  • Cleanup = critical part of useEffect pattern

useEffect Cleanup (Continued)

Overview

Event listeners require cleanup to prevent memory leaks. Use named functions for addEventListener/removeEventListener, and return cleanup function from useEffect.

Core Concepts

Event Listeners in JavaScript

Basic usage:

window.addEventListener('scroll', () => {
  console.log('You scrolled')
})

Problem with removal:

window.addEventListener('scroll', () => {
  console.log('You scrolled')
})

// ❌ Does NOT work
window.removeEventListener('scroll', () => {
  console.log('You scrolled')
})

Why it fails: Each () => {...} creates different function reference.

JavaScript requirement: Must pass same function reference to both add and remove.

Named Function Solution

Correct approach:

// Define named function
function handleScroll() {
  console.log('You scrolled')
}

// Pass reference to add
window.addEventListener('scroll', handleScroll)

// Pass same reference to remove
// ✅ Event will be removed
window.removeEventListener('scroll', handleScroll)

Key: Function identity matters for removeEventListener.

React Implementation (Wrong)

Code with memory leak:

import { useEffect } from 'react'

function App() {
  useEffect(() => {
    // ❌ Memory leak
    window.addEventListener('scroll', () => {
      console.log('You scrolled')
    })
  }, [])

  return <div>App</div>
}

Problem: Every component render adds event listener without cleanup.

Memory leak: Listeners accumulate, never removed.

Must provide cleanup: Call removeEventListener in cleanup function.

React Implementation (Correct)

Fixed with cleanup:

import { useEffect } from 'react'

function App() {
  useEffect(() => {
    function handleScroll() {
      console.log('You scrolled')
    }

    window.addEventListener('scroll', handleScroll)

    return () => {
      // ✅ Cleanup: no memory leak
      window.removeEventListener('scroll', handleScroll)
    }
  }, [])

  return <div>App</div>
}

Pattern:

  1. Define named function inside useEffect
  2. Add event listener with function reference
  3. Return cleanup function that removes listener
  4. Use same function reference in remove call

Scope: handleScroll defined in useEffect, accessible in cleanup closure.

StrictMode with Event Listeners

Development behavior: Component renders twice (StrictMode).

Flow:

  1. Component mounts → event listener added
  2. Component unmounts → cleanup runs → listener removed
  3. Component mounts again → event listener added

Result: Only one listener active.

Without cleanup: Two listeners (fires twice on scroll).

Scroll vs Click for Demos

Scroll/resize events: Fire very frequently.

Problem: Hard to notice double-firing in StrictMode.

Solution in exercises: Use click instead of scroll.

Why click better: Easier to observe duplicate event handlers (logs twice per click).

When Cleanup Is Needed

Don't always need cleanup: Some effects safe without it.

Confetti example (no cleanup needed):

useEffect(() => {
    confetti({...});
}, []);

Why no cleanup: Confetti stops automatically when element removed.

StrictMode behavior:

  1. Component renders → confetti fires → element removed immediately
  2. Component renders again → confetti fires (this one visible)

No visual difference: First confetti removed before seen.

Memory leak effects (need cleanup):

  • .addEventListener() - listeners persist
  • setTimeout - timers persist
  • setInterval - intervals persist
  • Fetch requests (covered next chapter)
  • WebSocket connections
  • Subscriptions

setTimeout with Cleanup

Timer example with dependencies:

Original setInterval (from previous lesson):

useEffect(() => {
  // ⚠️ Memory leak
  setInterval(() => {
    setCount((c) => c + 1)
  }, 1_000)
}, [])

Alternative with setTimeout:

useEffect(() => {
  // ⚠️ Memory leak
  setTimeout(() => {
    setCount((c) => c + 1)
  }, 1_000)
}, [count]) // Re-synchronize when count changes

Pattern: After first render, queue setCount in 1,000ms. When count changes, effect re-synchronizes (queues another timeout).

Still needs cleanup: Must provide way to cancel timeout.

Cleanup function:

useEffect(() => {
  const timeoutId = setTimeout(() => {
    setCount((c) => c + 1)
  }, 1_000)

  return () => {
    clearTimeout(timeoutId) // ✅ Cancel timer
  }
}, [count])

Cleanup runs: Before every re-synchronization (when count changes).

Flow:

  1. Effect runs → timeout scheduled
  2. Count changes → cleanup runs (cancels previous timeout) → new timeout scheduled
  3. Repeat

Prevents: Multiple queued timeouts (memory leak).

Usage Examples

Window resize listener:

import { useEffect, useState } from 'react'

function WindowSize() {
  const [width, setWidth] = useState(window.innerWidth)

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth)
    }

    window.addEventListener('resize', handleResize)

    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, [])

  return <p>Width: {width}px</p>
}

Click listener (easier to test):

function ClickTracker() {
  useEffect(() => {
    function handleClick() {
      console.log('Document clicked')
    }

    document.addEventListener('click', handleClick)

    return () => {
      document.removeEventListener('click', handleClick)
    }
  }, [])

  return <div>Tracking clicks</div>
}

Multiple listeners:

function MultiEventTracker() {
  useEffect(() => {
    function handleScroll() {
      console.log('Scrolled')
    }

    function handleKeyPress() {
      console.log('Key pressed')
    }

    window.addEventListener('scroll', handleScroll)
    window.addEventListener('keypress', handleKeyPress)

    return () => {
      window.removeEventListener('scroll', handleScroll)
      window.removeEventListener('keypress', handleKeyPress)
    }
  }, [])

  return <div>Tracking events</div>
}

setTimeout with state dependency:

function AutoIncrement() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const id = setTimeout(() => {
      setCount(count + 1)
    }, 1_000)

    return () => clearTimeout(id)
  }, [count]) // Re-run when count changes

  return <p>Count: {count}</p>
}

Conditional listener:

function ConditionalListener({ enabled }) {
  useEffect(() => {
    if (!enabled) return

    function handleClick() {
      console.log('Clicked')
    }

    document.addEventListener('click', handleClick)

    return () => {
      document.removeEventListener('click', handleClick)
    }
  }, [enabled])

  return <div>Listener {enabled ? 'active' : 'inactive'}</div>
}

Wrong - anonymous function removal fails:

function WrongPattern() {
  useEffect(() => {
    // ❌ Different function references
    window.addEventListener('scroll', () => console.log('scroll'))

    return () => {
      // This does NOTHING (different function)
      window.removeEventListener('scroll', () => console.log('scroll'))
    }
  }, [])

  return <div>Wrong pattern</div>
}

Listener with state access:

function StatefulListener() {
  const [message, setMessage] = useState('Hello')

  useEffect(() => {
    function handleClick() {
      console.log(message) // Access state
    }

    window.addEventListener('click', handleClick)

    return () => {
      window.removeEventListener('click', handleClick)
    }
  }, [message]) // Re-subscribe when message changes

  return <input value={message} onChange={(e) => setMessage(e.target.value)} />
}

Summary

  • Event listeners require cleanup to prevent memory leaks
  • addEventListener: Adds listener, persists until removed
  • removeEventListener: Must receive same function reference to work
  • Anonymous functions create new references each time (removal fails)
  • Solution: Define named function, use reference in add/remove
  • Pattern in React:
    1. Define function inside useEffect
    2. addEventListener with function reference
    3. Return cleanup calling removeEventListener with same reference
  • Function accessible in cleanup via closure
  • StrictMode flow: Mount → add listener → unmount → cleanup removes → mount → add listener
  • Without cleanup: Multiple listeners accumulate (fire multiple times)
  • Scroll/resize: Hard to notice duplication (fire too frequently)
  • Click events: Easier to demo memory leaks (obvious double-logging)
  • When cleanup needed: addEventListener, setTimeout, setInterval, subscriptions
  • When NOT needed: Effects that stop automatically (like confetti)
  • Confetti removed with element (no persistent side effect)
  • setTimeout with dependencies: Cleanup cancels previous timeout before new one
  • Cleanup runs before re-synchronization (when dependencies change)
  • clearTimeout: Cancels scheduled timeout using ID
  • Memory leak sources: Uncleaned listeners, timers, subscriptions
  • Must tell React how to stop effects that create persistent side effects
  • ESLint (local) warns about missing dependencies
  • Fetch in useEffect problematic (covered next chapter)
  • Key principle: If effect creates something persistent, cleanup must remove it
  • Same cleanup pattern for all persistent side effects

Summary

Effect Cleanup:

  • Some effects require way to stop them (cleanup)
  • Return function definition from useEffect for cleanup
  • Cleanup function called when component removed from DOM
  • Called before effect re-synchronizes

Memory Leaks:

  • StrictMode helps find memory leaks via double-rendering
  • If effect might create memory leak, must provide cleanup
  • If effect no extra side effect when executed twice, cleanup optional

Event Listeners:

  • Must pass same function reference to .removeEventListener() as .addEventListener()
  • Use named functions, not anonymous
  • Critical for proper cleanup

Pattern:

  • Provide way to start and stop effect
  • React handles re-synchronization automatically
  • Cleanup used in Concurrent rendering and other React features

Important Note:

  • Effect start/stop may be called more than expected
  • Beyond developer control (concurrent rendering)
  • React ensures components pure and free from side effects
  • Cleanup sometimes called unnecessarily but not a problem

Summary

useEffect Complete Overview (4 Chapters):

  • Hook for synchronizing React components with external systems
  • Provide start (effect function) and stop (cleanup) instructions
  • React manages execution automatically
  • Dependencies: Control when re-synchronization occurs
  • Empty [] = run once, [value] = run when value changes
  • Cleanup function: Return function to stop effect
  • StrictMode: Double-renders to expose memory leaks (mount-unmount-mount)
  • Helps catch missing cleanup functions during development

useEffect as Escape Hatch:

  • Use sparingly, only for external system synchronization
  • Previous guidance: Common usage
  • Current guidance (React 19, Concurrent rendering): Minimal usage
  • Not for: Data fetching (use React Query/SWR), computed values (rendering phase), general logic

Avoid useEffect for Data Fetching:

  • Must provide request cancellation (cleanup)
  • Code becomes complex (AbortController, signal handling, error catching)
  • Effects run after render (late for data needed immediately)
  • No built-in caching strategy (React Query/SWR provide this)
  • Not Suspense-compatible
  • Use @tanstack/react-query, SWR, or framework solutions (Next.js)

Fetch in useEffect Example (Avoid):

// ⚠️ Works but NOT recommended
useEffect(() => {
  const controller = new AbortController()
  const { signal } = controller

  fetch(url, { signal })
    .then((res) => res.json())
    .then((json) => {
      if (!signal.aborted) setData(json)
    })
    .catch((error) => {
      if (error.name !== 'AbortError') throw error
    })

  return () => controller.abort() // Cleanup
}, [])

Problems: Complex, manual abort handling, no caching, not Suspense-ready, late execution

Key Takeaways:

  • useEffect = synchronization tool, not general-purpose hook
  • Prioritize event handlers over effects
  • Use data-fetching libraries, not raw fetch in useEffect
  • StrictMode catches cleanup bugs (don't disable it)
  • Concurrent rendering requires proper cleanup
  • Correct usage: Analytics, third-party libraries, browser APIs, WebSockets
  • Incorrect usage: Fetching data, computed values, logic that belongs in render phase

Summary

useEffect Best Practices:

  • Escape hatch - use sparingly
  • Avoid for side-effect-free code (write in rendering phase instead)
  • Avoid for data fetching

Data Fetching:

  • Use library: @tanstack/react-query or SWR
  • React Frameworks (Next.js) have dedicated fetch methods
  • Don't use useEffect for fetch

StrictMode:

  • Helps catch bugs from missing cleanup functions
  • Important for finding memory leaks
  • Essential development tool

Future Usage:

  • Will use useEffect with refs (next chapter)
  • Will use in final project with React Router
  • More practice coming in real-world scenarios

useRef Hook

Overview

useRef is an escape hatch for accessing actual DOM elements directly. Use sparingly for imperative DOM operations like focusing, scrolling, or measuring elements.

Core Concepts

What Is useRef?

Hook purpose: Escape hatch from React into actual DOM.

Use cases - imperative DOM manipulation:

  • Focusing input elements (input.focus())
  • Scrolling to position (element.scrollTo(100, 200))
  • Scrolling into view (element.scrollIntoView())
  • Measuring size/position (element.getBoundingClientRect())

Why needed: These methods don't have React equivalents, require direct DOM access.

Key point: Use sparingly (escape hatch pattern).

Basic Setup

Import and create ref:

import { useRef } from 'react'

function App() {
  const inputRef = useRef(null)

  return <input type="text" placeholder="Your name" />
}

useRef argument: Initial value of reference (null typically).

Variable naming: Convention: inputRef, selectRef, canvasRef (match element type).

Attaching Ref to Element

Pass ref prop:

import { useRef } from 'react'

function App() {
  const inputRef = useRef(null)

  return <input type="text" placeholder="Your name" ref={inputRef} />
}

ref={inputRef} prop: Connects ref to DOM element.

Result: Can access underlying DOM element through ref.

ref.current Property

useRef returns object:

{
  current: null
}

Initial value: First execution uses provided argument (null).

After component renders and DOM created:

{
    current: <input type="text" placeholder="Your name"> // DOM element reference
}

To access DOM node: Use inputRef.current.

Key structure: Always access via .current property.

Focusing Input Example

Complete implementation:

import { useRef } from 'react'

function App() {
  const inputRef = useRef(null)

  function handleFocus() {
    inputRef.current.focus()
  }

  return (
    <>
      <input type="text" placeholder="Your name" ref={inputRef} />
      <button onClick={handleFocus}>Focus</button>
    </>
  )
}

How it works:

  1. User clicks "Focus" button
  2. handleFocus executes
  3. inputRef.current.focus() called
  4. Input element focuses

inputRef.current: Returns reference to underlying DOM element.

.focus() method: JavaScript method from HTMLInputElement interface (browser API).

Gotchas and Best Practices

Where to access refs:

  • ✅ Inside event handlers
  • ✅ Inside effects (useEffect)
  • ⚠️ Avoid in rendering phase

Rendering phase issue:

function App() {
  const inputRef = useRef(null)

  // ⚠️ First render: inputRef.current is null
  console.log(inputRef.current) // null initially

  return <input ref={inputRef} />
}

Why null initially: Element doesn't exist in DOM yet, ref set after render.

Recommendation: Keep ref usage to events and effects.

If you need ref value in JSX: Likely should use useState instead.

Usage Examples

Scroll to position:

import { useRef } from 'react'

function ScrollableList() {
  const listRef = useRef(null)

  function handleScrollToTop() {
    listRef.current.scrollTo(0, 0)
  }

  return (
    <>
      <div ref={listRef} style={{ height: '300px', overflow: 'auto' }}>
        {/* Long list content */}
      </div>
      <button onClick={handleScrollToTop}>Scroll to Top</button>
    </>
  )
}

Scroll into view:

function CommentSection() {
  const commentRef = useRef(null)

  function handleScrollToComment() {
    commentRef.current.scrollIntoView({ behavior: 'smooth' })
  }

  return (
    <>
      <button onClick={handleScrollToComment}>Go to Comment</button>
      <div ref={commentRef}>Important comment here</div>
    </>
  )
}

Measure element:

function ImageGallery() {
  const imageRef = useRef(null)

  function handleGetSize() {
    const rect = imageRef.current.getBoundingClientRect()
    console.log('Width:', rect.width, 'Height:', rect.height)
  }

  return (
    <>
      <img ref={imageRef} src="photo.jpg" alt="Photo" />
      <button onClick={handleGetSize}>Get Size</button>
    </>
  )
}

Multiple refs:

function MultiInputForm() {
  const emailRef = useRef(null)
  const passwordRef = useRef(null)

  function handleFocusEmail() {
    emailRef.current.focus()
  }

  function handleFocusPassword() {
    passwordRef.current.focus()
  }

  return (
    <form>
      <input ref={emailRef} type="email" placeholder="Email" />
      <button type="button" onClick={handleFocusEmail}>
        Focus Email
      </button>

      <input ref={passwordRef} type="password" placeholder="Password" />
      <button type="button" onClick={handleFocusPassword}>
        Focus Password
      </button>
    </form>
  )
}

Select text in input:

function TextInput() {
  const inputRef = useRef(null)

  function handleSelectAll() {
    inputRef.current.select()
  }

  return (
    <>
      <input ref={inputRef} defaultValue="Select me" />
      <button onClick={handleSelectAll}>Select All</button>
    </>
  )
}

Video control:

function VideoPlayer() {
  const videoRef = useRef(null)

  function handlePlay() {
    videoRef.current.play()
  }

  function handlePause() {
    videoRef.current.pause()
  }

  return (
    <div>
      <video ref={videoRef} src="video.mp4" />
      <button onClick={handlePlay}>Play</button>
      <button onClick={handlePause}>Pause</button>
    </div>
  )
}

Canvas drawing:

function Canvas() {
  const canvasRef = useRef(null)

  function handleDraw() {
    const ctx = canvasRef.current.getContext('2d')
    ctx.fillRect(10, 10, 100, 100)
  }

  return (
    <>
      <canvas ref={canvasRef} width={200} height={200} />
      <button onClick={handleDraw}>Draw</button>
    </>
  )
}

Wrong - accessing in render:

function WrongPattern() {
  const inputRef = useRef(null)

  // ❌ Don't do this: null on first render
  const value = inputRef.current?.value

  return <input ref={inputRef} />
}

Correct - useState for render values:

function CorrectPattern() {
  const [value, setValue] = useState('')

  // ✅ Use state for values needed in JSX
  return <input value={value} onChange={(e) => setValue(e.target.value)} />
}

Summary

  • useRef: Escape hatch for direct DOM access
  • Use sparingly for imperative operations
  • Use cases: focus(), scrollTo(), scrollIntoView(), getBoundingClientRect(), play/pause, etc.
  • Import: import {useRef} from "react"
  • Create ref: const refName = useRef(null)
  • Attach to element: <input ref={refName} />
  • useRef returns: Object with current property
  • Initial value: {current: null}
  • After render: {current: <DOM element>}
  • Access DOM element: refName.current
  • Call methods: refName.current.focus(), refName.current.scrollTo(), etc.
  • Access refs in: Event handlers, useEffect (not rendering phase)
  • First render: ref.current is null (element doesn't exist yet)
  • Ref set after component renders and DOM updated
  • Naming convention: inputRef, selectRef, buttonRef, etc.
  • Multiple refs: Create separate useRef for each element
  • If need value in JSX: Use useState, not useRef
  • useRef is escape hatch - don't overuse
  • Methods called are browser DOM APIs, not React
  • Next: useRef with useEffect (auto-focus after render)

useRef with useEffect

Overview

Combine useRef with useEffect for operations that need to run after component renders, like auto-focusing inputs. Use empty dependency array to focus once only.

Core Concepts

Common Use Case: Auto-Focus

Scenario: Focus input element after component renders.

Why useEffect needed: Synchronize component with external system (actual DOM).

External system: The DOM element (outside React's control).

Basic Implementation

First attempt:

import { useRef, useEffect } from 'react'

function App() {
  const inputRef = useRef(null)

  // ⚠️ Work in progress
  useEffect(() => {
    inputRef.current.focus()
  })

  return <input type="text" placeholder="Your name" ref={inputRef} />
}

Works, but: Has subtle issue.

Problem: Synchronizing too frequently (re-focuses on every re-render).

Dependency Array Issue

Current behavior: Effect runs after every render.

If component re-renders: Input focuses again (unnecessary).

Solution: Only focus after first render.

Correct Implementation

Add empty dependency array:

import { useRef, useEffect } from 'react'

function App() {
  const inputRef = useRef(null)

  useEffect(() => {
    inputRef.current.focus()
  }, []) // Empty array = run once

  return <input type="text" placeholder="Your name" ref={inputRef} />
}

Empty array []: Effect runs once after first render, never again.

Reference: Dependency arrays covered in Chapter 44.

Result: Input focuses once when component mounts, not on re-renders.

Cleanup Not Needed

Why no cleanup: .focus() behavior cancelled when element removed from DOM.

Calling focus twice: No extra side effects.

Safe pattern: No memory leak risk.

More Gotchas

useRef is escape hatch: Don't overuse refs.

Critical rule: Avoid changing DOM elements managed by React.

Safe operations (no DOM modification):

  • .focus() - changes focus state, not structure
  • .scrollIntoView() - scrolls viewport, not DOM
  • .scrollTo() - scrolls element, not structure
  • .getBoundingClientRect() - reads, doesn't modify

Unsafe operations (modify React-managed DOM):

  • .remove() - removes element
  • .innerHTML = ... - changes content
  • .appendChild() - adds elements
  • .classList.add() - modifies classes

Why unsafe: React loses track of DOM state, causes unexpected failures.

Usage Examples

Auto-focus on mount:

import { useRef, useEffect } from 'react'

function SearchBar() {
  const searchRef = useRef(null)

  useEffect(() => {
    searchRef.current.focus()
  }, [])

  return <input ref={searchRef} type="search" placeholder="Search..." />
}

Auto-scroll to element:

function Announcement() {
  const announcementRef = useRef(null)

  useEffect(() => {
    announcementRef.current.scrollIntoView({ behavior: 'smooth' })
  }, [])

  return (
    <div ref={announcementRef} className="announcement">
      Important message!
    </div>
  )
}

Multiple refs with effects:

function LoginForm() {
  const emailRef = useRef(null)
  const errorRef = useRef(null)

  useEffect(() => {
    emailRef.current.focus()
  }, [])

  useEffect(() => {
    if (errorRef.current) {
      errorRef.current.scrollIntoView()
    }
  }, [])

  return (
    <>
      <input ref={emailRef} type="email" />
      <div ref={errorRef} className="error">
        Error message
      </div>
    </>
  )
}

Conditional focusing:

function ConditionalFocus({ shouldFocus }) {
  const inputRef = useRef(null)

  useEffect(() => {
    if (shouldFocus) {
      inputRef.current.focus()
    }
  }, [shouldFocus]) // Re-run when shouldFocus changes

  return <input ref={inputRef} />
}

Play video on mount:

function AutoplayVideo({ src }) {
  const videoRef = useRef(null)

  useEffect(() => {
    videoRef.current.play()
  }, [])

  return <video ref={videoRef} src={src} />
}

Measure element after render:

function MeasuredBox() {
  const boxRef = useRef(null)

  useEffect(() => {
    const rect = boxRef.current.getBoundingClientRect()
    console.log('Box dimensions:', rect.width, rect.height)
  }, [])

  return <div ref={boxRef}>Content</div>
}

Wrong - re-focusing on every render:

function InefficientFocus() {
  const inputRef = useRef(null)

  useEffect(() => {
    inputRef.current.focus()
  }) // ❌ No dependencies: runs on every render

  return <input ref={inputRef} />
}

Wrong - modifying React-managed DOM:

function UnsafePattern() {
  const divRef = useRef(null)

  useEffect(() => {
    // ❌ DANGEROUS: modifies DOM React manages
    divRef.current.innerHTML = '<p>New content</p>'
  }, [])

  return <div ref={divRef}>Original content</div>
}

Correct - use state instead:

function SafePattern() {
  const [content, setContent] = useState('Original content')

  useEffect(() => {
    // ✅ Modify through state, not direct DOM
    setContent('New content')
  }, [])

  return <div>{content}</div>
}

Scroll to top on mount:

function ScrollToTop() {
  const containerRef = useRef(null)

  useEffect(() => {
    containerRef.current.scrollTo(0, 0)
  }, [])

  return (
    <div ref={containerRef} style={{ height: '500px', overflow: 'auto' }}>
      Long content here...
    </div>
  )
}

Summary

  • Common pattern: useRef + useEffect for post-render DOM operations
  • Use case: Auto-focus input after component renders
  • Need useEffect because: Synchronizing with external system (DOM)
  • First attempt issue: Effect runs on every render (too frequent)
  • Solution: Empty dependency array []
  • useEffect(() => {...}, []) runs once after first render only
  • Empty array = "don't re-synchronize"
  • Prevents unnecessary re-focusing on state updates
  • No cleanup needed for .focus(): Cancelled automatically when element removed
  • Calling focus multiple times: No side effects
  • Always check dependencies: Avoid unnecessary synchronizations
  • useRef = escape hatch: Don't overuse
  • Safe operations: focus(), scrollIntoView(), scrollTo(), getBoundingClientRect()
  • Unsafe operations: Changing DOM structure/content React manages
  • ❌ Never: .remove(), .innerHTML, .appendChild(), direct class/style changes
  • Why unsafe: React loses track of DOM, causes failures
  • If need to change content/structure: Use useState and JSX, not direct DOM
  • Pattern: Import useRef + useEffect → create ref → attach with ref prop → access in effect
  • Multiple refs: Each can have own useEffect
  • Conditional effects: Use dependencies to control when effect runs
  • Auto-focus improves UX (common accessibility pattern)
  • Remember: Refs for reading/calling methods, state for data changes

Summary

useRef Hook:

  • Escape hatch from React into actual DOM
  • Use sparingly
  • Returns object with current key: useRef(initialValue)
  • Attach to DOM: ref={refName} prop
  • Access DOM methods: ref.current.focus()

ref.current Behavior:

  • First access in rendering phase: null
  • Safe to access in events or effects
  • Generally access inside events or effects

Common Pattern:

  • Focus input after component renders
  • Combine useRef with useEffect
  • Check useEffect dependencies to avoid unnecessary synchronizations

Important Warnings:

  • useRef is escape hatch - don't overuse
  • Code shouldn't have too many refs
  • Avoid changing DOM elements managed by React
  • Only use for imperative operations (focus, scroll, measure)

Summary

Project XII - AutoFocus Login:

  • Improved Login page from Project IX
  • Auto-focus email input using useRef + useEffect
  • Pattern: useRef(null) → attach with ref={} → focus in useEffect(..., [])

useEffect & useRef as Escape Hatches:

  • Both considered escape hatches (use sparingly)
  • Don't overuse - keep refs minimal
  • Purpose: Specific imperative DOM operations only

Multiple useEffect in Component:

  • One component can have multiple useEffect hooks
  • Best practice: One useEffect per synchronization
  • Separate concerns: Each effect handles one external system
  • Example: One for analytics, another for confetti

Pattern:

function App({ theme }) {
  const [count, setCount] = useState(10)

  useEffect(() => {
    logAnalytics('/') // Synchronization 1
  }, [])

  useEffect(() => {
    confetti({ particleCount: count }) // Synchronization 2
  }, [count])
}

Optional Chaining for Refs:

  • Use when ref might be null (conditional rendering)
  • Syntax: inputRef.current?.focus()
  • Prevents calling methods on null
  • current always exists, but value may be null
  • Safer than if checks for method calls

Next: Context for avoiding prop drilling


Prop Drilling

Overview

Prop drilling occurs when passing props through multiple intermediate components to reach deeply nested children. While recommended, it can become cumbersome - Context provides alternative.

Core Concepts

Lifting State Review

From Chapter on Lifting State Up: Raise state to first common ancestor.

Recommended approach: Generally best way to handle state.

Problem: Can lead to prop drilling.

What Is Prop Drilling?

Definition: Passing props through multiple intermediate components just to get data from parent to deeply nested child.

Key characteristic: Intermediate components don't need the data themselves (only forwarding).

Can be cumbersome: Many layers of prop passing.

Example Problem

Component structure:

App (has user state)
├── Navbar (needs user)
└── PageContainer
    ├── Sidebar
    └── Layout
        └── Settings (needs user)

Initial code:

import { useState } from 'react'

function App() {
  const [user, setUser] = useState(null)

  return (
    <>
      <Navbar user={user} />
      <PageContainer />
    </>
  )
}

function PageContainer() {
  return (
    <>
      <Sidebar />
      <Layout />
    </>
  )
}

function Layout() {
  return <Settings />
}

function Settings() {
  /* 🤔 How do we get 'user' here? */
  return <p>Welcome user</p>
}

Problem: Settings needs user, but several components in between.

Prop Drilling Solution

Pass through every layer:

import { useState } from 'react'

function App() {
  const [user, setUser] = useState(null)

  return (
    <>
      <Navbar user={user} />
      <PageContainer user={user} /> {/* Pass down */}
    </>
  )
}

function PageContainer({ user }) {
  /* Receive */
  return (
    <>
      <Sidebar />
      <Layout user={user} /> {/* Pass down */}
    </>
  )
}

function Layout({ user }) {
  /* Receive */
  return <Settings user={user} />
  {
    /* Pass down */
  }
}

function Settings({ user }) {
  /* Finally use it */
  return <p>Welcome {user}</p>
}

What happened: PageContainer and Layout don't use user, just forward it.

This is prop drilling: Props passed through intermediaries.

Is Prop Drilling Bad?

No - it's recommended: Prop drilling is generally the correct approach.

Why recommended: Predictable, explicit data flow.

Use as much as possible: Default to prop drilling.

When to reconsider: Becomes unmanageable or too cumbersome.

Alternative: Context (explained next lesson).

Scaling the Problem

Adding more props:

import { useState } from 'react'

function App() {
  const [user, setUser] = useState(null)

  function onUserLogin() {
    /* ... */
  }
  function onUserLogout() {
    /* ... */
  }

  return (
    <>
      <Navbar
        user={user}
        onUserLogin={onUserLogin}
        onUserLogout={onUserLogout}
      />
      <PageContainer
        user={user}
        onUserLogin={onUserLogin}
        onUserLogout={onUserLogout}
      />
    </>
  )
}

function PageContainer({ user, onUserLogin, onUserLogout }) {
  return (
    <>
      <Sidebar />
      <Layout
        user={user}
        onUserLogin={onUserLogin}
        onUserLogout={onUserLogout}
      />
    </>
  )
}

function Layout({ user, onUserLogin, onUserLogout }) {
  return (
    <Settings
      user={user}
      onUserLogin={onUserLogin}
      onUserLogout={onUserLogout}
    />
  )
}

function Settings({ user, onUserLogin, onUserLogout }) {
  return <p>Welcome {user}</p>
}

Still manageable: Only 3 props through 2-3 layers.

Real codebases: Imagine 5x more props, deeper nesting.

Becomes unmanageable: Too much repetition, hard to maintain.

Context Preview

How Context helps: Components directly access data without intermediate passing.

With Context (preview):

App (provides user, handlers)
├── Navbar (receives via Context) ✅
└── PageContainer
    ├── Sidebar
    └── Layout
        └── Settings (receives via Context) ✅

Intermediate components: Don't need to know about or pass props.

Data access: Direct from provider to consumer.

Next lesson: How Context works.

Usage Examples

Simple prop drilling (manageable):

function App() {
  const [theme, setTheme] = useState('light')

  return <Parent theme={theme} />
}

function Parent({ theme }) {
  return <Child theme={theme} /> // Just forwarding
}

function Child({ theme }) {
  return <div className={theme}>Content</div> // Actually uses it
}

Deeper nesting (more drilling):

function App() {
  const [settings, setSettings] = useState({})

  return <Level1 settings={settings} />
}

function Level1({ settings }) {
  return <Level2 settings={settings} />
}

function Level2({ settings }) {
  return <Level3 settings={settings} />
}

function Level3({ settings }) {
  return <Level4 settings={settings} />
}

function Level4({ settings }) {
  return <ActualUser settings={settings} />
}

function ActualUser({ settings }) {
  return <div>Using {settings.theme}</div>
}

Multiple props drilled:

function App() {
  const [user, setUser] = useState(null)
  const [theme, setTheme] = useState('light')
  const [language, setLanguage] = useState('en')

  return (
    <Layout
      user={user}
      theme={theme}
      language={language}
      onThemeChange={setTheme}
      onLanguageChange={setLanguage}
    />
  )
}

function Layout({ user, theme, language, onThemeChange, onLanguageChange }) {
  return (
    <Main
      user={user}
      theme={theme}
      language={language}
      onThemeChange={onThemeChange}
      onLanguageChange={onLanguageChange}
    />
  )
}

function Main({ user, theme, language, onThemeChange, onLanguageChange }) {
  return (
    <Settings
      user={user}
      theme={theme}
      language={language}
      onThemeChange={onThemeChange}
      onLanguageChange={onLanguageChange}
    />
  )
}

function Settings({ user, theme, language, onThemeChange, onLanguageChange }) {
  return <div>Settings for {user?.name}</div>
}

When it becomes cumbersome: 5+ props through 3+ levels.

Real-world example:

// Imagine this pattern repeated across entire app
function App() {
  const auth = useAuth()
  const permissions = usePermissions()
  const config = useConfig()
  const analytics = useAnalytics()

  // Everything must be drilled down...
  return <FeatureA {...allProps} />
}

Summary

  • Prop drilling: Passing props through multiple intermediates to reach nested child
  • Intermediate components don't use props, just forward them
  • Lifted state: Causes prop drilling (state at common ancestor, children need it)
  • Is it bad? No - prop drilling is recommended approach
  • Why recommended: Predictable, explicit, easy to trace data flow
  • Use prop drilling as default/primary pattern
  • When cumbersome: Many props through many layers
  • Example: 3 props (user, onUserLogin, onUserLogout) through 2-3 components
  • Manageable: Small number of props, few layers
  • Unmanageable: 5x props in real codebase, deeper nesting
  • Alternative solution: Context (next lesson)
  • Context lets components access data directly without intermediate passing
  • Don't rush to Context: Try prop drilling first
  • Only use Context when prop drilling becomes too cumbersome
  • Trade-off: Prop drilling = explicit, Context = implicit (less clear data flow)
  • Pattern: State at top → drill down → use at bottom
  • Intermediate components = "middlemen" (receive and pass, don't use)
  • Real codebases: More props + deeper nesting = harder maintenance
  • Key principle: Prefer simplicity (prop drilling) until complexity requires alternative
  • Next: How Context solves prop drilling problem

React Context

Overview

Context shares data between components without prop drilling through multiple levels. Works in 3 steps: create context, wrap components, consume from nested children.

Core Concepts

What Is Context?

Purpose: Share data between components without passing props through multiple levels.

Alternative to: Prop drilling (previous lesson problem).

React feature: Built-in data sharing mechanism.

Three-Step Pattern

Steps:

  1. Create the context
  2. Wrap components with context
  3. Consume context from nested components

Starting code:

function Settings() {
  return <p>Hello user</p>
}

function App() {
  return <Settings />
}

Goal: Share user data from App to Settings without prop drilling.

Step 1: Create Context

Import createContext:

import { createContext } from 'react'

const UserContext = createContext(null)

function Settings() {
  return <p>Hello user</p>
}

function App() {
  return <Settings />
}

createContext(defaultValue): Receives default value argument.

Default value: Used when consuming context outside provider (explained later).

For now: Use null as placeholder.

Naming convention: PascalCase (UpperCamelCase) required.

Why PascalCase: Will use in JSX as <UserContext>, components must start with capital.

Example: UserContext (not userContext) enables <UserContext /> usage.

Step 2: Wrap Components

Wrap children with context:

import { createContext } from 'react'

const UserContext = createContext(null)

function Settings() {
  return <p>Hello user</p>
}

function App() {
  const name = 'awesome-student'

  return (
    <UserContext value={{ name: name }}>
      <Settings />
    </UserContext>
  )
}

Pattern: <UserContext value={{...}}>children</UserContext>

value prop: Data to share (object {name: "awesome-student"}).

Object shorthand: value={{name}} equivalent to value={{name: name}}.

Double curly braces: Required for object props (outer = JSX expression, inner = object literal).

Effect: Any child component can access {name: "awesome-student"}.

React 19 change: No longer need <UserContext.Provider> (old pattern).

Old pattern (React <19):

<UserContext.Provider value={{ name }}>...</UserContext.Provider>

New pattern (React 19):

<UserContext value={{ name }}>...</UserContext>

Step 3: Consume Context

Import useContext hook:

import { createContext, useContext } from 'react'

const UserContext = createContext(null)

function Settings() {
  const context = useContext(UserContext)
  return <p>Hello {context.name}</p>
}

function App() {
  const name = 'awesome-student'

  return (
    <UserContext value={{ name: name }}>
      <Settings />
    </UserContext>
  )
}

useContext(YourContext): Hook to access context value.

Returns: Value passed to value prop ({name: "awesome-student"}).

Usage: context.name accesses the name property.

Result: Renders <p>Hello awesome-student</p>.

defaultValue Explained

When used: Consuming context outside provider.

Example:

const UserContext = createContext({ name: '' })

Better default: Match structure of actual value (e.g., {name: ""} instead of null).

Scenario: Calling useContext(UserContext) from component not wrapped in <UserContext>.

Returns: Default value from createContext().

Best practice: Provide sensible defaults matching expected shape.

Context Scope

Available to: Children components only, not current component.

Example:

function App() {
  const name = 'awesome-student'

  return (
    <UserContext value={{ name: name }}>
      <Settings />
    </UserContext>
  )
}

Cannot do: Call useContext(UserContext) inside App component.

If you try: Returns defaultValue, not provided value.

Why: Context available to children, not component rendering the provider.

Correct: Consume in Settings (child of provider).

Incorrect: Consume in App (component rendering provider).

Usage Examples

Theme context:

import { createContext, useContext } from 'react'

const ThemeContext = createContext('light')

function Button() {
  const theme = useContext(ThemeContext)
  return <button className={theme}>Click me</button>
}

function App() {
  return (
    <ThemeContext value="dark">
      <Button />
    </ThemeContext>
  )
}

Multiple consumers:

const UserContext = createContext(null)

function Header() {
  const user = useContext(UserContext)
  return <h1>Welcome {user.name}</h1>
}

function Profile() {
  const user = useContext(UserContext)
  return <p>Email: {user.email}</p>
}

function App() {
  return (
    <UserContext value={{ name: 'Alex', email: '[email protected]' }}>
      <Header />
      <Profile />
    </UserContext>
  )
}

Nested components:

const DataContext = createContext(null)

function GrandChild() {
  const data = useContext(DataContext)
  return <p>{data.message}</p>
}

function Child() {
  return <GrandChild /> // Doesn't need to pass props
}

function Parent() {
  return <Child /> // Doesn't need to pass props
}

function App() {
  return (
    <DataContext value={{ message: 'Hello from context' }}>
      <Parent />
    </DataContext>
  )
}

Multiple contexts:

const UserContext = createContext(null)
const ThemeContext = createContext('light')

function Component() {
  const user = useContext(UserContext)
  const theme = useContext(ThemeContext)
  return <div className={theme}>Hello {user.name}</div>
}

function App() {
  return (
    <UserContext value={{ name: 'Sam' }}>
      <ThemeContext value="dark">
        <Component />
      </ThemeContext>
    </UserContext>
  )
}

With default value:

const ConfigContext = createContext({
  apiUrl: 'https://api.default.com',
  timeout: 5000,
})

function DataFetcher() {
  const config = useContext(ConfigContext)
  // Uses default if not wrapped in provider
  return <p>API: {config.apiUrl}</p>
}

Consuming in multiple places:

const CartContext = createContext({ items: [] })

function CartIcon() {
  const cart = useContext(CartContext)
  return <span>Cart ({cart.items.length})</span>
}

function CartPage() {
  const cart = useContext(CartContext)
  return (
    <div>
      <h1>Cart</h1>
      {cart.items.map((item) => (
        <p key={item.id}>{item.name}</p>
      ))}
    </div>
  )
}

function App() {
  const cartData = { items: [{ id: 1, name: 'Apple' }] }

  return (
    <CartContext value={cartData}>
      <CartIcon />
      <CartPage />
    </CartContext>
  )
}

Summary

  • Context: Share data without prop drilling through multiple levels
  • Three steps: Create, wrap, consume
  • Step 1 - Create: const YourContext = createContext(defaultValue)
  • Import createContext from "react"
  • Use PascalCase naming (required for JSX usage)
  • Step 2 - Wrap: <YourContext value={{...}}>children</YourContext>
  • Wrap components that need access
  • Pass data via value prop
  • React 19: No .Provider needed (simplified API)
  • Step 3 - Consume: const context = useContext(YourContext)
  • Import useContext hook
  • Call with context object
  • Returns value from nearest provider above
  • defaultValue: Used when consuming outside provider
  • Best practice: Match expected data structure
  • Scope: Context available to children, not current component
  • Cannot consume in component rendering provider
  • Double curly braces: {{name}} = JSX expression containing object
  • Object shorthand: {name} equivalent to {name: name}
  • Any number of children can consume same context
  • No need to pass props through intermediates
  • Solves prop drilling problem from previous lesson
  • Next: Refactoring context to separate file

Summary

Prop Drilling Problem:

  • Lifting state up can lead to prop drilling
  • Passing props through multiple intermediates to reach nested child
  • Recommended approach but can become cumbersome

Context Solution:

  • Share data between components without multi-level prop passing
  • Use when prop drilling becomes unmanageable

Context 3-Step Pattern:

  1. Create: const YourContext = createContext(defaultValue);
  2. Wrap: <YourContext value={{...}}>...</YourContext>
  3. Consume: const context = useContext(YourContext);

Default Value:

  • Used when useContext() called from component NOT child of provider
  • Fallback for components outside provider tree

Context Data Availability:

  • Available to children components
  • NOT available to current component providing it
  • Must be nested inside provider to access

Refactoring Context to Separate File

Overview

Move context definition to separate file for better organization. Export context to use across multiple files. Prepares for custom provider pattern in next lesson.

Core Concepts

Why Refactor?

Previous lesson: All code in single file (simplification).

This lesson: Move to separate file for organization.

Benefit: Easier to manage, reuse across app.

Next lesson preparation: Custom provider implementation easier with separate file.

Starting Code

Code from previous lesson:

import { createContext, useContext } from 'react'

const UserContext = createContext(null)

function Settings() {
  const context = useContext(UserContext)
  return <p>Hello {context.name}</p>
}

function App() {
  const name = 'awesome-student'

  return (
    <UserContext value={{ name: name }}>
      <Settings />
    </UserContext>
  )
}

Problem: Context mixed with component code.

Goal: Separate concerns - context in own file.

Step 1: Create Context File

New file: UserContext.jsx

Move context creation:

// UserContext.jsx
import { createContext } from 'react'

export const UserContext = createContext(null)

Changes:

  1. Import createContext (move from index.jsx)
  2. Add export keyword before const UserContext
  3. Only context definition in this file

export keyword: Makes UserContext available to other files.

Named export: export const UserContext (not default export).

Step 2: Import in Component File

Update index.jsx:

import { useContext } from 'react'
import { UserContext } from './UserContext'

function Settings() {
  const context = useContext(UserContext)
  return <p>Hello {context.name}</p>
}

function App() {
  const name = 'awesome-student'

  return (
    <UserContext value={{ name: name }}>
      <Settings />
    </UserContext>
  )
}

Changes:

  1. Remove createContext import
  2. Add import {UserContext} from "./UserContext" (named import)
  3. Keep useContext import

Import syntax: Curly braces for named exports {UserContext}.

File path: "./UserContext" (relative path, .jsx extension optional).

Named Export/Import Pattern

Export (UserContext.jsx):

export const UserContext = createContext(null)

Import (index.jsx):

import { UserContext } from './UserContext'

Named import syntax: Must use curly braces {}.

Name must match: Import name matches export name.

Multiple named exports possible:

// UserContext.jsx
export const UserContext = createContext(null)
export const ThemeContext = createContext('light')

// index.jsx
import { UserContext, ThemeContext } from './UserContext'

Why This Refactor?

Immediate benefit: Separation of concerns.

Future benefit: Makes custom provider pattern easier (next lesson).

Scalability: Context logic separate from component logic.

Reusability: Import context in any file that needs it.

Pattern: Common in React apps - contexts in own files.

Usage Examples

Multiple context exports:

// contexts/AppContexts.jsx
import { createContext } from 'react'

export const UserContext = createContext(null)
export const ThemeContext = createContext('light')
export const LanguageContext = createContext('en')

Import multiple:

// components/Header.jsx
import { useContext } from 'react'
import { UserContext, ThemeContext } from '../contexts/AppContexts'

function Header() {
  const user = useContext(UserContext)
  const theme = useContext(ThemeContext)
  return <header className={theme}>Welcome {user.name}</header>
}

Separate files per context:

// contexts/UserContext.jsx
import { createContext } from 'react'
export const UserContext = createContext(null)

// contexts/ThemeContext.jsx
import { createContext } from 'react'
export const ThemeContext = createContext('light')

// components/App.jsx
import { UserContext } from '../contexts/UserContext'
import { ThemeContext } from '../contexts/ThemeContext'

With TypeScript (preview):

// UserContext.tsx
import { createContext } from 'react'

interface User {
  name: string
  email: string
}

export const UserContext = createContext<User | null>(null)

Folder organization:

src/
  contexts/
    UserContext.jsx
    CartContext.jsx
    ThemeContext.jsx
  components/
    App.jsx
    Header.jsx
    Settings.jsx

Import from organized folders:

// components/Settings.jsx
import { useContext } from 'react'
import { UserContext } from '../contexts/UserContext'

function Settings() {
  const user = useContext(UserContext)
  return <div>Settings for {user.name}</div>
}

Re-exporting from index:

// contexts/index.js
export { UserContext } from './UserContext'
export { CartContext } from './CartContext'
export { ThemeContext } from './ThemeContext'

// components/App.jsx
import { UserContext, CartContext } from '../contexts'

Default value in separate file:

// contexts/ConfigContext.jsx
import { createContext } from 'react'

const defaultConfig = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3,
}

export const ConfigContext = createContext(defaultConfig)

Summary

  • Refactoring: Move context from component file to separate file
  • Purpose: Better organization, separation of concerns
  • Pattern: Contexts typically in own files
  • Step 1: Create YourContext.jsx file
  • Step 2: Move context creation with export keyword
  • Export syntax: export const YourContext = createContext(...)
  • Import syntax: import {YourContext} from "./YourContext"
  • Named export/import (curly braces required)
  • Import name must match export name
  • File extension optional in import path
  • Remove createContext import from component file
  • Keep useContext import where consuming
  • Multiple exports: Can export multiple contexts from one file
  • Organization: Common to have contexts/ folder
  • Simplifies next lesson: custom provider pattern
  • Best practice: Separate context definition from component logic
  • Makes context reusable across app
  • Easier to maintain and update
  • Preparation: Sets up for dynamic context with state (next lesson)
  • Minor refactor with significant organizational benefit

Context with State

Overview

Create dynamic contexts with state by defining custom providers. Custom providers wrap children, manage state, and provide dynamic values and functions to all nested components.

Core Concepts

Static vs Dynamic Context

Previous contexts: Static values that never change.

This lesson: Dynamic context with state and event handlers.

Goal: Provide state values and functions that update state.

Context Provider

Automatic creation: createContext() creates provider automatically.

Default provider limitation: Can't return dynamic values.

Solution: Create custom provider component.

Custom provider: Component managing state and providing dynamic values.

Step 1: Define Custom Provider

In UserContext.jsx:

// UserContext.jsx
import { createContext } from 'react'

export const UserContext = createContext(null)

export function UserProvider() {}

UserProvider: Custom component for wrapping app.

export: Makes provider available to other files.

Purpose: Provide dynamic values (state, functions) to children.

Step 2: Use Custom Provider

Before (static context):

// index.jsx
import { UserContext } from './UserContext'

function App() {
  const name = 'awesome-student'

  return (
    <UserContext value={{ name: name }}>
      <Settings />
    </UserContext>
  )
}

After (custom provider):

// index.jsx
import { UserContext, UserProvider } from './UserContext'

function App() {
  return (
    <UserProvider>
      <Settings />
    </UserProvider>
  )
}

Changes:

  1. Import UserProvider (named export)
  2. Replace <UserContext value={...}> with <UserProvider>
  3. Remove name state from App

Status: Empty provider (work in progress), next step: implement.

Step 3: Implement Provider - Children Prop

Usage pattern:

<UserProvider>
  <Settings />
</UserProvider>

Provider receives: Children components to render.

Children prop: props.children contains nested components.

Basic implementation:

// UserContext.jsx
import { createContext } from 'react'

export const UserContext = createContext(null)

export function UserProvider(props) {
  return <UserContext>{props.children}</UserContext>
}

Pattern: Provider wraps props.children with <UserContext>.

Result: Children can access context (once we add value).

Step 4: Add State

Create state in provider:

// UserContext.jsx
import { createContext, useState } from 'react'

export const UserContext = createContext(null)

export function UserProvider(props) {
  const [name, setName] = useState('awesome-student')

  return <UserContext value={{ name }}>{props.children}</UserContext>
}

useState: State lives in custom provider.

value prop: Pass state to context.

Effect: All children can access name state.

Dynamic: State changes propagate to all consumers.

Step 5: Add Functions

Provide functions for state updates:

// UserContext.jsx
import { createContext, useState } from 'react'

export const UserContext = createContext(null)

export function UserProvider(props) {
  const [name, setName] = useState('awesome-student')

  function handleNameReset() {
    setName('')
  }

  return (
    <UserContext value={{ name, handleNameReset }}>
      {props.children}
    </UserContext>
  )
}

value object: {name, handleNameReset} (object shorthand).

Available to consumers: Both state and function.

Pattern: Encapsulate state and logic in provider.

Consuming Dynamic Context

In index.jsx:

// index.jsx
import { useContext } from 'react'
import { UserContext, UserProvider } from './UserContext'

function Settings() {
  const context = useContext(UserContext)
  console.log(context)
  // {name: "awesome-student", handleNameReset: () => ...}

  return (
    <>
      <p>Hello {context.name}</p>
      <button onClick={context.handleNameReset}>Reset</button>
    </>
  )
}

function App() {
  return (
    <UserProvider>
      <Settings />
    </UserProvider>
  )
}

Context value: Object with name (state) and handleNameReset (function).

Button click: Calls handleNameReset → updates state → all consumers re-render.

Re-render behavior: State change triggers re-render of all context consumers.

Usage Examples

Counter context:

// CounterContext.jsx
import { createContext, useState } from 'react'

export const CounterContext = createContext(null)

export function CounterProvider(props) {
  const [count, setCount] = useState(0)

  function increment() {
    setCount(count + 1)
  }

  function decrement() {
    setCount(count - 1)
  }

  return (
    <CounterContext value={{ count, increment, decrement }}>
      {props.children}
    </CounterContext>
  )
}

// App.jsx
function Counter() {
  const { count, increment, decrement } = useContext(CounterContext)
  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  )
}

function App() {
  return (
    <CounterProvider>
      <Counter />
    </CounterProvider>
  )
}

Theme context with toggle:

// ThemeContext.jsx
import { createContext, useState } from 'react'

export const ThemeContext = createContext(null)

export function ThemeProvider(props) {
  const [theme, setTheme] = useState('light')

  function toggleTheme() {
    setTheme(theme === 'light' ? 'dark' : 'light')
  }

  return (
    <ThemeContext value={{ theme, toggleTheme }}>{props.children}</ThemeContext>
  )
}

// Component.jsx
function ThemeToggle() {
  const { theme, toggleTheme } = useContext(ThemeContext)
  return <button onClick={toggleTheme}>Current: {theme}</button>
}

Multiple state values:

// UserContext.jsx
export function UserProvider(props) {
  const [user, setUser] = useState(null)
  const [isLoggedIn, setIsLoggedIn] = useState(false)

  function login(userData) {
    setUser(userData)
    setIsLoggedIn(true)
  }

  function logout() {
    setUser(null)
    setIsLoggedIn(false)
  }

  return (
    <UserContext value={{ user, isLoggedIn, login, logout }}>
      {props.children}
    </UserContext>
  )
}

Complex cart context:

// CartContext.jsx
export function CartProvider(props) {
  const [items, setItems] = useState([])

  function addItem(product) {
    setItems([...items, product])
  }

  function removeItem(productId) {
    setItems(items.filter((item) => item.id !== productId))
  }

  function clearCart() {
    setItems([])
  }

  const totalPrice = items.reduce((sum, item) => sum + item.price, 0)

  return (
    <CartContext value={{ items, totalPrice, addItem, removeItem, clearCart }}>
      {props.children}
    </CartContext>
  )
}

With derived values:

export function TodoProvider(props) {
  const [todos, setTodos] = useState([])

  function addTodo(text) {
    setTodos([...todos, { id: Date.now(), text, done: false }])
  }

  function toggleTodo(id) {
    setTodos(
      todos.map((todo) =>
        todo.id === id ? { ...todo, done: !todo.done } : todo,
      ),
    )
  }

  const completedCount = todos.filter((t) => t.done).length
  const pendingCount = todos.filter((t) => !t.done).length

  return (
    <TodoContext
      value={{
        todos,
        addTodo,
        toggleTodo,
        completedCount,
        pendingCount,
      }}
    >
      {props.children}
    </TodoContext>
  )
}

Destructuring in consumer:

function Component() {
  // Instead of: const context = useContext(UserContext);
  // Use destructuring:
  const { name, handleNameReset } = useContext(UserContext)

  return (
    <div>
      <p>{name}</p>
      <button onClick={handleNameReset}>Reset</button>
    </div>
  )
}

Summary

  • Previous contexts: Static values only
  • Dynamic context: State + functions that update state
  • createContext(): Automatically creates provider, but can't return dynamic values
  • Custom provider: Component managing state and providing dynamic values
  • Pattern: Export both context and custom provider
  • Step 1: Define export function YourProvider(props)
  • Step 2: Import and use <YourProvider>children</YourProvider>
  • Step 3: Implement provider - wrap props.children with <YourContext>
  • props.children: Contains nested components to render
  • Step 4: Add useState in provider
  • Step 5: Create functions, pass to value prop
  • value object: {state, functions} using object shorthand
  • State lives in provider, accessible to all children
  • Functions update state, trigger re-renders
  • Consuming: useContext(YourContext) returns object with state + functions
  • Click handlers can call context functions
  • State changes propagate to all consumers (they re-render)
  • Encapsulation: State logic in provider, consumers just use it
  • Cleaner than prop drilling for shared state
  • Multiple state values and functions possible
  • Can include derived values (computed from state)
  • Destructuring: const {value, func} = useContext(Ctx) for cleaner code
  • Pattern: Provider wraps app/section, components consume as needed
  • Next: Real project using this pattern (CartContext)

Summary

Context Organization:

  • Good practice: define context in separate file
  • Must export context to use in other files
  • Improves code organization and reusability

Context Provider:

  • Component making value available to all nested components
  • Default provider doesn't allow dynamic values

Custom Provider:

  • Function wrapping application providing dynamic values
  • Receives child components as props.children
  • Can have state and functions to update state
  • Pattern:
export function SomeProvider(props) {
  const [value, setValue] = useState()
  function handleSomething() {}
  return (
    <SomeContext value={{ value, handleSomething }}>
      {props.children}
    </SomeContext>
  )
}

Boilerplate Structure:

  • Import createContext, useState
  • Export context and custom provider
  • Change placeholder names
  • Wrap app (or part of app) with provider

Scoping:

  • Can wrap entire app or just part
  • Provider gives child components access to context data
  • Only wrap common ancestor if context data needed in specific section

Summary

Project XIII - Cart Context Implementation:

  • Created CartContext for SuperM app
  • Provides: cart state, cartCount, cartSum, handleAddProduct, handleRemoveProduct
  • Used in <Navbar /> (cart count), <ProductDetails /> (add product), <Cart /> (add/remove)
  • Alternative to prop drilling for cart functionality

Context Re-render Behavior:

  • Every component subscribed to context (useContext(YourContext)) re-renders when provider re-renders
  • Cart state change → all useContext(CartContext) consumers re-render
  • Performance consideration: May impact larger apps

Multiple Contexts Strategy:

  • Create separate contexts for different concerns:
    • CartContext - cart-related values
    • UserContext - current user/authentication
    • AppSettingsContext - language, theme, font size, etc.
  • Reduces unnecessary re-renders (components only subscribe to contexts they need)
  • Don't worry yet: Performance optimization covered in optional chapters

Context vs Prop Drilling:

  • Context provides flexibility (no intermediate prop passing)
  • Use when: Many components need same data, deep nesting, data shared across app
  • Prop drilling still valid for simpler cases

Next: React Router for navigation and routing


Introduction to React Router

Overview

React Router provides declarative routing for React applications. Define routes with components that automatically render based on URL, enabling multi-page navigation without full-page reloads.

Core Concepts

What Is React Router?

Purpose: Library for declarative routing in React.

Declarative routing: Declare what renders for each route, not how.

Example preview:

<BrowserRouter>
  <Routes>
    <Route path="/" element={<LandingPage />} />
    <Route path="/about" element={<AboutPage />} />
  </Routes>
</BrowserRouter>

Behavior:

  • User visits / → renders <LandingPage />
  • User visits /about → renders <AboutPage />
  • Automatic based on URL

Declarative Pattern

Definition: Declare routes without explicitly dictating rendering process.

What you specify: Path and component to render.

React Router handles: Detecting URL changes, rendering correct component.

Declarative (React Router):

<Route path="/about" element={<AboutPage />} />

Imperative alternative (manual): Use state, useEffect, browser APIs to watch URL and conditionally render.

Benefit: React Router provides common features out of the box.

Package Details

Package name: react-router

Size: 160kb minified (relatively large).

Import pattern:

import { BrowserRouter, Routes, Route } from 'react-router'

Components imported: Core routing components (explained next lesson).

DIY vs Library

Could implement yourself: Using state, useEffect, browser history API.

React Router provides:

  • Route matching
  • URL parameters
  • Nested routes
  • Navigation without page reload
  • Browser history integration
  • Link components
  • Route protection
  • And more features

Why use library: Save time, battle-tested, comprehensive feature set.

React Tutorial Integration

URL preview: React Tutorial browser tab now shows URL.

Example URL: http://localhost/about

Base URL: Always localhost in environment.

Important: Look at path after localhost (/, /about, /products, etc.).

Visual aid: Browser preview shows current route.

Usage Examples

Basic route structure:

import { BrowserRouter, Routes, Route } from 'react-router'

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Routes>
    </BrowserRouter>
  )
}

Multi-page app behavior:

  • Navigate to /<Home /> renders
  • Navigate to /about<About /> renders
  • Navigate to /contact<Contact /> renders
  • No page reload (SPA behavior)

Simple components:

function Home() {
  return <h1>Welcome Home</h1>
}

function About() {
  return <h1>About Us</h1>
}

function Contact() {
  return <h1>Contact Us</h1>
}

E-commerce routes:

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Landing />} />
    <Route path="/products" element={<ProductList />} />
    <Route path="/cart" element={<Cart />} />
    <Route path="/checkout" element={<Checkout />} />
  </Routes>
</BrowserRouter>

Blog routes:

<BrowserRouter>
  <Routes>
    <Route path="/" element={<BlogHome />} />
    <Route path="/post" element={<PostList />} />
    <Route path="/about" element={<About />} />
    <Route path="/contact" element={<Contact />} />
  </Routes>
</BrowserRouter>

Dashboard routes:

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Dashboard />} />
    <Route path="/settings" element={<Settings />} />
    <Route path="/profile" element={<Profile />} />
    <Route path="/logout" element={<Logout />} />
  </Routes>
</BrowserRouter>

Summary

  • React Router: Library for declarative routing in React
  • Declarative: Declare routes (path + component), React Router handles rendering
  • Pattern: <Route path="/about" element={<AboutPage />} />
  • User navigates → URL changes → React Router renders matching component
  • No page reload: Single-page app (SPA) behavior
  • Package: react-router, 160kb minified
  • Import: import {BrowserRouter, Routes, Route} from "react-router"
  • Alternative to manual: Building with state + useEffect + browser APIs
  • React Router provides: Route matching, params, nested routes, navigation, history, links, etc.
  • Benefits: Time-saving, comprehensive features, battle-tested
  • React Tutorial: Browser preview shows URL (look at path after localhost)
  • Next lesson: How to define routes in detail
  • Following chapters: Linking between routes, URL parameters, nested routes, more features
  • Enables multi-page feel in single-page app
  • Essential for modern React applications with navigation

Defining Routes in React Router

Overview

Routes define which component renders for each URL path. Use BrowserRouter, Routes, and Route components from React Router to create declarative routing structure.

Core Concepts

The Three Components

1. BrowserRouter:

  • Root router component
  • Uses browser history API
  • Wraps entire app (or routing section)
  • Most popular router (alternatives: MemoryRouter, HashRouter, StaticRouter)
  • Recommendation: Use BrowserRouter for web apps

2. Routes:

  • Container component that decides which route to render
  • Must be inside <BrowserRouter>
  • Wraps all <Route> components
  • Selects matching route based on current URL

3. Route:

  • Defines single route
  • Must be direct child of Routes (no wrappers allowed)
  • Two required props: path and element

BrowserRouter

Purpose: Router using browser history API.

Placement: Wrap around routes (entire app or routing section).

Import:

import { BrowserRouter } from 'react-router'

Alternative routers:

  • MemoryRouter: In-memory routing (testing, React Native)
  • HashRouter: URL hash routing (#/about)
  • StaticRouter: Server-side rendering

Best choice: BrowserRouter for standard web apps.

Example:

<BrowserRouter>{/* Routes go here */}</BrowserRouter>

Routes

Purpose: Container that decides which route renders.

Behavior: Looks at current URL, finds matching <Route>, renders its element.

Requirement: Must be inside <BrowserRouter>.

Import:

import { Routes } from 'react-router'

Example:

<BrowserRouter>
  <Routes>{/* Route components here */}</Routes>
</BrowserRouter>

How it works:

  • User navigates to /about
  • <Routes> checks all child <Route> paths
  • Finds match: <Route path="/about" element={<About />} />
  • Renders <About /> component

Route

Purpose: Declares single route (path → component).

Two required props:

1. path prop:

  • URL path to match
  • Relative to parent (or absolute starting with /)
  • Examples: "/", "/about", "/products", "/contact"

2. element prop:

  • JSX component to render when path matches
  • Must be JSX element: <About /> not About
  • Pass props like any component: <About name="Sam" />

Requirement: Must be direct child of <Routes>.

Cannot wrap in div or other element:

{
  /* ❌ WRONG */
}
;<Routes>
  <div>
    <Route path="/" element={<Home />} />
  </div>
</Routes>

{
  /* ✅ CORRECT */
}
;<Routes>
  <Route path="/" element={<Home />} />
</Routes>

Import:

import { Route } from 'react-router'

Example:

<Route path="/about" element={<About />} />

Breakdown:

  • path="/about" - Match URL /about
  • element={<About />} - Render <About /> component

Complete Pattern

Full import:

import { BrowserRouter, Routes, Route } from 'react-router'

Full structure:

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
    <Route path="/products" element={<Products />} />
  </Routes>
</BrowserRouter>

Behavior:

  • Visit /<Home /> renders
  • Visit /about<About /> renders
  • Visit /products<Products /> renders

Usage Examples

Basic three-route app:

import { BrowserRouter, Routes, Route } from 'react-router'

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Landing />} />
        <Route path="/about" element={<About />} />
        <Route path="/products" element={<Products />} />
      </Routes>
    </BrowserRouter>
  )
}

function Landing() {
  return <h1>Welcome</h1>
}

function About() {
  return <h1>About Us</h1>
}

function Products() {
  return <h1>Our Products</h1>
}

E-commerce routes:

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/shop" element={<Shop />} />
    <Route path="/cart" element={<Cart />} />
    <Route path="/checkout" element={<Checkout />} />
    <Route path="/account" element={<Account />} />
  </Routes>
</BrowserRouter>

Blog routes:

<BrowserRouter>
  <Routes>
    <Route path="/" element={<BlogHome />} />
    <Route path="/posts" element={<PostList />} />
    <Route path="/categories" element={<Categories />} />
    <Route path="/about" element={<AboutBlog />} />
    <Route path="/contact" element={<Contact />} />
  </Routes>
</BrowserRouter>

Dashboard routes:

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Dashboard />} />
    <Route path="/analytics" element={<Analytics />} />
    <Route path="/settings" element={<Settings />} />
    <Route path="/users" element={<UserManagement />} />
    <Route path="/reports" element={<Reports />} />
  </Routes>
</BrowserRouter>

Passing props to routes:

<Routes>
  <Route path="/" element={<Home greeting="Welcome!" />} />
  <Route path="/about" element={<About company="ACME Corp" />} />
  <Route path="/contact" element={<Contact email="[email protected]" />} />
</Routes>

Authentication routes:

<BrowserRouter>
  <Routes>
    <Route path="/" element={<PublicHome />} />
    <Route path="/login" element={<Login />} />
    <Route path="/signup" element={<Signup />} />
    <Route path="/dashboard" element={<Dashboard />} />
    <Route path="/profile" element={<Profile />} />
  </Routes>
</BrowserRouter>

Multi-page feel:

// Visit http://localhost/ → Landing renders
// Visit http://localhost/about → About renders
// Visit http://localhost/products → Products renders
// All without page reload (SPA)

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Landing />} />
    <Route path="/about" element={<About />} />
    <Route path="/products" element={<Products />} />
  </Routes>
</BrowserRouter>

Component with state:

function Products() {
  const [filter, setFilter] = useState('')

  return (
    <div>
      <h1>Products</h1>
      <input
        value={filter}
        onChange={(event) => setFilter(event.target.value)}
        placeholder="Filter products..."
      />
    </div>
  )
}

;<Route path="/products" element={<Products />} />

Summary

  • Three core components: BrowserRouter, Routes, Route
  • BrowserRouter: Root router using browser history API (most popular choice)
  • Alternatives: MemoryRouter, HashRouter, StaticRouter
  • Use BrowserRouter for standard web apps
  • Routes: Container deciding which route to render based on URL
  • Must be inside <BrowserRouter>
  • Checks current URL against all child <Route> paths
  • Route: Declares single route (path → component)
  • Two required props: path (URL) and element (JSX component)
  • element must be JSX: <About /> not About
  • Must be direct child of Routes (no wrapper divs)
  • Import: import {BrowserRouter, Routes, Route} from "react-router"
  • Pattern:
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
    
  • URL changes → Routes finds match → Renders element
  • No page reload: Single-page app behavior
  • Pass props to route components like any component
  • Next lessons: Linking between routes, URL parameters, nested routes
  • Foundation for building navigable React applications

Linking to Routes in React Router

Overview

The Link component from React Router creates navigation links without full page reloads. Use Link with the to prop to navigate between routes declaratively.

Core Concepts

The Link Component

Purpose: Create navigation links in React Router.

Behavior: Renders <a> tag with JavaScript for history API navigation.

Why not regular <a> tags? React Router adds necessary behavior (no page reload, history management).

Import:

import { Link } from 'react-router'

Basic syntax:

<Link to="/">Back home</Link>

Breakdown:

  • to prop: Destination path (like href in <a>)
  • Content: Link text or children
  • Renders: <a href="/">Back home</a> with JavaScript behavior

Example:

<Link to="/about">Learn more</Link>

How Link Works

Rendered output: Semantic <a> tag with href attribute.

JavaScript behavior: React Router intercepts clicks, uses history API to change URL without reload.

History API: Browser feature for programmatic navigation.

Result: Single-page app (SPA) experience with instant navigation.

Without Link: Regular <a> tag causes full page reload.

With Link: URL changes, component swaps, no reload.

Example rendered HTML:

<a href="/about">Learn more</a>

But with added: Click handler that prevents default, updates history, triggers React Router.

The to Prop

Purpose: Specify destination path.

Type: String (path).

Examples:

<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/products">Products</Link>
<Link to="/contact">Contact</Link>

Relative paths: Work from current route (advanced feature).

Absolute paths: Start with / for root-relative.

Dynamic paths: Can use template strings or variables.

Main Navigation Pattern

Common pattern: Wrap navigation links in semantic HTML.

Recommended structure:

<nav>
  <ul>
    <li>
      <Link to="/">Home</Link>
    </li>
    <li>
      <Link to="/about">About</Link>
    </li>
    <li>
      <Link to="/products">Products</Link>
    </li>
  </ul>
</nav>

Why <nav> element? Semantic HTML indicating main navigation.

Accessibility: Screen readers announce navigation landmark.

Styling: Common CSS target for navigation styles.

Flexibility: Can use other structures (div, header, etc.), but <nav> provides semantics.

Rendering Between BrowserRouter and Routes

Pattern: Elements between <BrowserRouter> and <Routes> render on all pages.

Structure:

<BrowserRouter>
  <nav>
    <Link to="/">Home</Link>
    <Link to="/about">About</Link>
  </nav>

  <main>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
    </Routes>
  </main>
</BrowserRouter>

Behavior:

  • <nav> renders on all routes (always visible)
  • <Routes> swaps content based on URL
  • Common for: Navigation, header, footer

Why this works: BrowserRouter wraps everything, Routes only controls route-specific content.

Benefit: Persistent elements (nav, header, footer) without duplication.

Important Constraint

Rule: <Route> must be direct child of <Routes>.

Cannot do:

{
  /* ❌ WRONG */
}
;<Routes>
  <div>
    <Route path="/" element={<Home />} />
  </div>
</Routes>

Must do:

{
  /* ✅ CORRECT */
}
;<Routes>
  <Route path="/" element={<Home />} />
</Routes>

Other elements: Can render between BrowserRouter and Routes (nav, header, etc.).

Flexibility: Wrap Routes in divs, main elements, etc., but Routes children must be Route components.

Usage Examples

Basic navigation:

import { BrowserRouter, Routes, Route, Link } from 'react-router'

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  )
}

function Home() {
  return (
    <div>
      <h1>Home</h1>
      <p>Welcome to our site!</p>
    </div>
  )
}

function About() {
  return (
    <div>
      <h1>About</h1>
      <Link to="/">Back home</Link>
    </div>
  )
}

Semantic navigation:

<nav>
  <ul>
    <li>
      <Link to="/">Home</Link>
    </li>
    <li>
      <Link to="/products">Products</Link>
    </li>
    <li>
      <Link to="/about">About</Link>
    </li>
    <li>
      <Link to="/contact">Contact</Link>
    </li>
  </ul>
</nav>

Header with nav:

<BrowserRouter>
  <header>
    <h1>My App</h1>
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Link to="/contact">Contact</Link>
    </nav>
  </header>

  <main>
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/contact" element={<Contact />} />
    </Routes>
  </main>

  <footer>
    <p>&copy; 2024 My App</p>
  </footer>
</BrowserRouter>

Back link in component:

function ProductDetails() {
  return (
    <div>
      <h1>Product Details</h1>
      <p>Product information here...</p>
      <Link to="/products">Back to products</Link>
    </div>
  )
}

Navigation with icons:

<nav>
  <Link to="/">🏠 Home</Link>
  <Link to="/shop">🛍️ Shop</Link>
  <Link to="/cart">🛒 Cart</Link>
  <Link to="/profile">👤 Profile</Link>
</nav>

Conditional nav rendering:

function Navigation({ isLoggedIn }) {
  return (
    <nav>
      <Link to="/">Home</Link>
      {isLoggedIn ? (
        <>
          <Link to="/dashboard">Dashboard</Link>
          <Link to="/logout">Logout</Link>
        </>
      ) : (
        <Link to="/login">Login</Link>
      )}
    </nav>
  )
}

Full app structure:

import { BrowserRouter, Routes, Route, Link } from 'react-router'

function App() {
  return (
    <BrowserRouter>
      {/* Persistent header - visible on all routes */}
      <header>
        <h1>E-Commerce App</h1>
      </header>

      {/* Persistent navigation - visible on all routes */}
      <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/products">Products</Link>
          </li>
          <li>
            <Link to="/cart">Cart</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
        </ul>
      </nav>

      {/* Main content - swaps based on route */}
      <main>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/products" element={<Products />} />
          <Route path="/cart" element={<Cart />} />
          <Route path="/about" element={<About />} />
        </Routes>
      </main>

      {/* Persistent footer - visible on all routes */}
      <footer>
        <p>&copy; 2024 E-Commerce App</p>
      </footer>
    </BrowserRouter>
  )
}

Navigation in sidebar:

<BrowserRouter>
  <div className="app-layout">
    <aside className="sidebar">
      <nav>
        <Link to="/">Dashboard</Link>
        <Link to="/analytics">Analytics</Link>
        <Link to="/settings">Settings</Link>
      </nav>
    </aside>

    <main className="content">
      <Routes>
        <Route path="/" element={<Dashboard />} />
        <Route path="/analytics" element={<Analytics />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </main>
  </div>
</BrowserRouter>

Link with state or data:

function ProductList({ products }) {
  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          <Link to={`/product/${product.id}`}>{product.name}</Link>
        </li>
      ))}
    </ul>
  )
}

Summary

  • Link component: Navigation without page reload
  • Import: import {Link} from "react-router"
  • Syntax: <Link to="/path">Text</Link>
  • to prop: Destination path (like href in <a>)
  • Renders: Semantic <a> tag with JavaScript behavior
  • React Router intercepts clicks, uses history API, prevents reload
  • Must use Link: Regular <a> tags cause full page reload
  • Main navigation pattern: Wrap in <nav><ul><li> for semantics/accessibility
  • Structure:
    <nav>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
      </ul>
    </nav>
    
  • Persistent elements: Render between BrowserRouter and Routes (visible all pages)
  • Common: Navigation, header, footer
  • Pattern:
    <BrowserRouter>
      <nav>...</nav>
      <Routes>
        <Route path="/" element={<Home />} />
      </Routes>
    </BrowserRouter>
    
  • Route constraint: Must be direct child of Routes (no wrapper divs)
  • Other elements flexible placement
  • Enables single-page app experience with instant navigation
  • Next lessons: URL parameters, nested routes, route protection
  • Foundation for building navigable React applications

Summary

React Router Basics:

  • Library providing declarative routing for React
  • Defines routes and components per route
  • Package size: 160kb minified
  • React Tutorial shows URL in browser preview tab

Core Components:

  • BrowserRouter: most popular router, uses browser history API
  • Routes: decides which Route to render based on URL
  • Route: declarative route definition with path and element props
  • Route must be direct child of Routes
  • element prop expects JSX component (<Landing />) not name (Landing)

Navigation:

  • Link component for navigation with to prop
  • Must use Link (not regular anchor tags)
  • Generates anchor using history API (no full page reload)
  • to prop must match route path
  • nav element: semantic element for navigation section

Layout Pattern:

  • Render components between BrowserRouter and Routes for all pages
  • Common for navigation, headers, footers
  • Elements outside Routes visible on every route

Framework Note:

  • React Router can be used on server
  • Check Framework mode for server-side usage

Route Parameters in React Router

Overview

Route parameters enable dynamic routes matching variable URL segments. Use :paramName syntax in route paths and the useParams() hook to access parameter values.

Core Concepts

What Are Route Parameters?

Purpose: Create dynamic routes with variable URL segments.

Example: /products/1, /products/2, /products/abc all match /products/:id.

Use cases:

  • Product details by ID
  • User profiles by username
  • Blog posts by slug
  • Any resource with unique identifier

Why needed: Avoid creating separate route for every item.

One route: /products/:id handles all product IDs.

Parameter Syntax

Pattern: Use colon : prefix in route path.

Definition:

<Route path="/products/:id" element={<ProductDetails />} />

Breakdown:

  • /products/ - Static part (must match exactly)
  • :id - Dynamic part (parameter, matches any text)

Colon meaning: "This is a parameter, not literal text."

Parameter name: Can be anything (:id, :userId, :slug, :productId).

Multiple parameters:

<Route path="/users/:userId/posts/:postId" element={<Post />} />

What Matches?

Pattern: /products/:id

Matches:

  • /products/1id = "1"
  • /products/42id = "42"
  • /products/abcid = "abc"
  • /products/shoe-123id = "shoe-123"
  • /products/anythingid = "anything"

Does NOT match:

  • /products/ (no segment after /products/)
  • /products/1/2 (extra segment)

Key insight: :id matches any text, not just numbers.

Parameter name :id: Has no special meaning (doesn't enforce numbers).

The useParams Hook

Purpose: Access route parameters in component.

Import:

import { useParams } from 'react-router'

Returns: Object with all parameters from current route.

Usage:

function ProductDetails() {
  const params = useParams()
  console.log(params) // {id: "1"}

  return <h1>Product {params.id}</h1>
}

Destructuring pattern (common):

function ProductDetails() {
  const { id } = useParams()

  return <h1>Product {id}</h1>
}

Multiple parameters:

function Post() {
  const { userId, postId } = useParams()

  return (
    <h1>
      User {userId}, Post {postId}
    </h1>
  )
}

Parameters Are Always Strings

Critical: Parameters extracted from URL are always strings.

Example URL: /products/1

Parameter value: "1" (string), not 1 (number).

Why? URLs only contain text, no automatic type conversion.

Examples:

// URL: /products/1
const { id } = useParams()
console.log(id) // "1" (string)
console.log(typeof id) // "string"

// URL: /products/abc
const { id } = useParams()
console.log(id) // "abc" (string)

Must convert manually:

const { id } = useParams()
const numericId = Number(id) // Convert to number
const parsedId = parseInt(id, 10) // Alternative conversion

Validation example:

function ProductDetails() {
  const { id } = useParams()
  const numericId = Number(id)

  if (isNaN(numericId)) {
    return <p>Invalid product ID</p>
  }

  return <h1>Product {numericId}</h1>
}

Common Patterns

Fetch data using parameter:

function ProductDetails() {
  const { id } = useParams()
  const [product, setProduct] = useState(null)

  useEffect(() => {
    fetch(`/api/products/${id}`)
      .then((res) => res.json())
      .then((data) => setProduct(data))
  }, [id])

  if (!product) return <p>Loading...</p>

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  )
}

React Query with parameter:

function ProductDetails() {
  const { id } = useParams()

  const { data: product } = useQuery({
    queryKey: ['product', id],
    queryFn: () => fetch(`/api/products/${id}`).then((res) => res.json()),
  })

  if (!product) return <p>Loading...</p>

  return <h1>{product.name}</h1>
}

Usage Examples

Basic route with parameter:

import { BrowserRouter, Routes, Route, useParams } from 'react-router'

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/products/:id" element={<ProductDetails />} />
      </Routes>
    </BrowserRouter>
  )
}

function ProductDetails() {
  const { id } = useParams()

  return (
    <div>
      <h1>Product Details</h1>
      <p>Showing product: {id}</p>
    </div>
  )
}

User profile by username:

;<Route path="/users/:username" element={<UserProfile />} />

function UserProfile() {
  const { username } = useParams()

  return <h1>Profile of {username}</h1>
}

// URL: /users/john → username = "john"
// URL: /users/sarah → username = "sarah"

Blog post by slug:

;<Route path="/blog/:slug" element={<BlogPost />} />

function BlogPost() {
  const { slug } = useParams()

  return <h1>Post: {slug}</h1>
}

// URL: /blog/intro-to-react → slug = "intro-to-react"
// URL: /blog/advanced-hooks → slug = "advanced-hooks"

Multiple parameters:

;<Route path="/users/:userId/posts/:postId" element={<Post />} />

function Post() {
  const { userId, postId } = useParams()

  return (
    <div>
      <h1>Post {postId}</h1>
      <p>By user: {userId}</p>
    </div>
  )
}

// URL: /users/123/posts/456
// userId = "123", postId = "456"

Type conversion and validation:

function ProductDetails() {
  const { id } = useParams()
  const numericId = Number(id)

  if (isNaN(numericId)) {
    return (
      <div>
        <h1>Invalid Product ID</h1>
        <p>Product ID must be a number</p>
      </div>
    )
  }

  return <h1>Product #{numericId}</h1>
}

Using parameter with useState:

function ProductDetails() {
  const { id } = useParams()
  const [product, setProduct] = useState(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    setLoading(true)
    fetch(`https://api.example.com/products/${id}`)
      .then((res) => res.json())
      .then((data) => {
        setProduct(data)
        setLoading(false)
      })
  }, [id])

  if (loading) return <p>Loading...</p>
  if (!product) return <p>Product not found</p>

  return (
    <div>
      <h1>{product.name}</h1>
      <p>Price: ${product.price}</p>
      <p>{product.description}</p>
    </div>
  )
}

Category and product:

;<Route
  path="/categories/:category/products/:productId"
  element={<ProductInCategory />}
/>

function ProductInCategory() {
  const { category, productId } = useParams()

  return (
    <div>
      <p>Category: {category}</p>
      <p>Product ID: {productId}</p>
    </div>
  )
}

// URL: /categories/electronics/products/42
// category = "electronics", productId = "42"

Navigation with params:

function ProductList() {
  const products = [
    { id: 1, name: 'Laptop' },
    { id: 2, name: 'Phone' },
    { id: 3, name: 'Tablet' },
  ]

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          <Link to={`/products/${product.id}`}>{product.name}</Link>
        </li>
      ))}
    </ul>
  )
}

Complex parameter:

;<Route path="/search/:query" element={<SearchResults />} />

function SearchResults() {
  const { query } = useParams()
  const decodedQuery = decodeURIComponent(query)

  return <h1>Search results for: {decodedQuery}</h1>
}

// URL: /search/react%20hooks
// query = "react hooks" (after decoding)

With React Query:

function ProductDetails() {
  const { id } = useParams()

  const {
    data: product,
    isLoading,
    isError,
  } = useQuery({
    queryKey: ['product', id],
    queryFn: async () => {
      const response = await fetch(`/api/products/${id}`)
      if (!response.ok) throw new Error('Failed to fetch')
      return response.json()
    },
  })

  if (isLoading) return <p>Loading...</p>
  if (isError) return <p>Error loading product</p>

  return (
    <div>
      <h1>{product.name}</h1>
      <p>${product.price}</p>
    </div>
  )
}

Date parameter:

;<Route path="/events/:date" element={<EventsByDate />} />

function EventsByDate() {
  const { date } = useParams()
  // date might be "2024-01-15"

  return <h1>Events on {date}</h1>
}

Summary

  • Route parameters: Dynamic URL segments in routes
  • Syntax: Use colon : prefix in route path
  • Example: <Route path="/products/:id" element={<ProductDetails />} />
  • Parameter name: Can be anything (:id, :userId, :slug, etc.)
  • Matches any text: /products/1, /products/abc, /products/anything
  • :id name has no special meaning (doesn't enforce numbers)
  • useParams hook: Access parameters in component
  • Import: import {useParams} from "react-router"
  • Returns object with all parameters
  • Common pattern: Destructure const {id} = useParams()
  • Parameters always strings: Extracted from URL as text
  • /products/1id is "1" (string, not number)
  • Must convert manually: Number(id) or parseInt(id, 10)
  • Validation required: Check if conversion valid (isNaN(), etc.)
  • Multiple parameters: path="/users/:userId/posts/:postId"
  • Destructure: const {userId, postId} = useParams()
  • Use cases: Product details, user profiles, blog posts, search results, any resource with ID
  • Common patterns: Fetch data, React Query, state management, validation
  • Next lessons: Nested routes, route protection, advanced patterns
  • Enables dynamic, data-driven routing in React applications
  • Essential for apps with many similar pages (products, users, posts, etc.)

Protecting Routes with Navigate

Overview

The Navigate component from React Router enables route protection by redirecting users to different pages. Use conditional logic to guard routes based on authentication state or other conditions.

Core Concepts

The Navigate Component

Purpose: Redirect users to a different route programmatically.

Use case: Protect routes that require authentication or authorization.

Import:

import { Navigate } from 'react-router'

Basic usage:

function UserDetails() {
  return <Navigate to="/login" replace />
}

Behavior: Always redirects to /login when component renders.

The replace Prop

Purpose: Controls browser history behavior during redirect.

With replace: Replaces current history entry (user can't go back).

Without replace: Adds new history entry (user can go back with browser back button).

Recommendation: Use replace for redirections (generally recommended).

Why replace? Prevents user from navigating back to protected page after redirect.

Example:

<Navigate to="/login" replace />

Browser behavior:

  • User visits /profile (not logged in)
  • Redirected to /login
  • Browser back button won't return to /profile

Conditional Redirects

Pattern: Use conditional logic to determine when to redirect.

Common use case: Check authentication state.

Example with conditional:

import { Navigate } from 'react-router'

function UserDetails(props) {
  if (!props.user) {
    return <Navigate to="/login" replace />
  }

  return <h1>Hello {props.user.name}</h1>
}

Breakdown:

  • Check if user prop exists
  • If no user → redirect to /login
  • If user exists → render user details

Early return pattern: Common React pattern for conditional rendering.

Route Protection Pattern

Scenario: /profile route should only be accessible when logged in.

Route definition:

<Route path="/profile" element={<UserDetails user={user} />} />

Component implementation:

function UserDetails(props) {
  if (!props.user) {
    return <Navigate to="/login" replace />
  }

  return (
    <div>
      <h1>User Profile</h1>
      <p>Welcome, {props.user.name}</p>
    </div>
  )
}

Behavior:

  • Logged in user → sees profile
  • Not logged in → redirected to login page

Alternative: Check at route level with wrapper component.

When to Use Navigate

Use Navigate when:

  • Protecting routes based on conditions
  • Redirecting after checking state/props
  • Implementing authentication guards
  • Handling authorization checks

Don't use Navigate when:

  • Responding to user events (use useNavigate hook instead)
  • Inside useEffect (use useNavigate hook)
  • After async operations (use useNavigate hook)

Navigate is declarative: Returns JSX, renders during component render.

Usage Examples

Basic redirect:

import { Navigate } from 'react-router'

function ProtectedPage() {
  return <Navigate to="/login" replace />
}

Authentication guard:

function Dashboard(props) {
  if (!props.isAuthenticated) {
    return <Navigate to="/login" replace />
  }

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Welcome to your dashboard!</p>
    </div>
  )
}

Using with Context:

import { useContext } from 'react'
import { Navigate } from 'react-router'
import { UserContext } from './UserContext'

function Profile() {
  const { user } = useContext(UserContext)

  if (!user) {
    return <Navigate to="/login" replace />
  }

  return (
    <div>
      <h1>Profile</h1>
      <p>Email: {user.email}</p>
    </div>
  )
}

Admin route protection:

function AdminPanel(props) {
  if (!props.user) {
    return <Navigate to="/login" replace />
  }

  if (props.user.role !== 'admin') {
    return <Navigate to="/" replace />
  }

  return <h1>Admin Panel</h1>
}

Multiple conditions:

function PremiumContent(props) {
  const { user, subscription } = props

  if (!user) {
    return <Navigate to="/login" replace />
  }

  if (!subscription || !subscription.active) {
    return <Navigate to="/upgrade" replace />
  }

  return (
    <div>
      <h1>Premium Content</h1>
      <p>Exclusive content for subscribers</p>
    </div>
  )
}

Reusable protected route wrapper:

function ProtectedRoute(props) {
  if (!props.user) {
    return <Navigate to="/login" replace />
  }

  return props.children
}

// Usage in routes
;<Route
  path="/profile"
  element={
    <ProtectedRoute user={user}>
      <Profile />
    </ProtectedRoute>
  }
/>

Email verification guard:

function VerifiedOnlyPage(props) {
  const { user } = props

  if (!user) {
    return <Navigate to="/login" replace />
  }

  if (!user.emailVerified) {
    return <Navigate to="/verify-email" replace />
  }

  return <h1>Verified Users Only Content</h1>
}

Redirect from login when already authenticated:

function Login(props) {
  if (props.user) {
    // Already logged in, redirect to home
    return <Navigate to="/" replace />
  }

  return (
    <form>
      <input type="email" placeholder="Email" />
      <input type="password" placeholder="Password" />
      <button>Login</button>
    </form>
  )
}

Role-based routing:

function UserManagement(props) {
  const { user } = props

  if (!user) {
    return <Navigate to="/login" replace />
  }

  if (user.role === 'user') {
    return <Navigate to="/dashboard" replace />
  }

  if (user.role === 'moderator') {
    return <ModeratorPanel />
  }

  if (user.role === 'admin') {
    return <AdminPanel />
  }

  return <Navigate to="/" replace />
}

Subscription check:

function PaidFeature(props) {
  if (!props.user) {
    return <Navigate to="/signup" replace />
  }

  if (props.user.plan === 'free') {
    return <Navigate to="/pricing" replace />
  }

  return <h1>Paid Feature Content</h1>
}

Summary

  • Navigate component: Redirects users to different routes
  • Import: import {Navigate} from "react-router"
  • Basic syntax: <Navigate to="/path" replace />
  • to prop: Destination route path
  • replace prop: Replaces history entry (prevents back navigation)
  • Recommended: Use replace for redirections
  • Without replace: User can use back button to return
  • Conditional redirects: Use if statements to check conditions
  • Pattern:
    if (!props.user) {
      return <Navigate to="/login" replace />
    }
    return <ActualContent />
    
  • Route protection: Guard routes based on authentication, authorization, subscription, etc.
  • Early return pattern: Check conditions first, redirect if failed, render content if passed
  • Use cases: Authentication guards, admin routes, premium content, verified users
  • When NOT to use: User events, useEffect, after async operations (use useNavigate hook)
  • Navigate is declarative: Returns JSX during render
  • Multiple conditions: Chain multiple if statements for complex guards
  • Reusable wrappers: Create <ProtectedRoute> wrapper components
  • Context integration: Works well with Context for global user state
  • Next lessons: Programmatic navigation with useNavigate hook
  • Foundation for building secure, authenticated React applications

Programmatic Navigation with useNavigate

Overview

The useNavigate() hook enables programmatic navigation for scenarios where JSX-based components like <Navigate> or <Link> aren't suitable. Use it for redirects triggered by events, effects, or async operations.

Core Concepts

What Is Programmatic Navigation?

Definition: Navigation triggered by code execution, not user clicking links.

Use cases:

  • After successful form submission
  • Following async operations (API calls)
  • Inside event handlers with logic
  • In useEffect after side effects
  • After logout/authentication
  • Complex navigation logic

Difference from declarative:

  • Declarative: <Link> and <Navigate> (return JSX)
  • Programmatic: Execute function to navigate

Why needed: Can't return JSX in event handlers or after async operations.

The useNavigate Hook

Purpose: Returns function for programmatic navigation.

Import:

import { useNavigate } from 'react-router'

Usage pattern:

function Component() {
  const navigate = useNavigate()

  function handleSomething() {
    // ... logic ...
    navigate('/path')
  }

  return <button onClick={handleSomething}>Action</button>
}

Returns: Navigation function.

Call at top level: Hook must follow rules of hooks (top level, not in conditions).

Use returned function anywhere: Can call navigate() inside conditions, events, effects.

Logout Example

Scenario: Clear localStorage then redirect to home.

Implementation:

import { useNavigate } from 'react-router'

function UserDetails(props) {
  const navigate = useNavigate()

  function handleLogout() {
    window.localStorage.clear()
    navigate('/')
  }

  return <button onClick={handleLogout}>Log out</button>
}

Breakdown:

  • useNavigate() called at top level
  • navigate() function stored in variable
  • handleLogout clears localStorage first
  • Then calls navigate("/") to redirect home
  • Button triggers logout flow

Why programmatic? Multiple actions before navigation (can't just return <Navigate>).

Navigation Function Usage

Basic navigation:

navigate('/dashboard')

With replace (no history entry):

navigate('/login', { replace: true })

Go back:

navigate(-1)

Go forward:

navigate(1)

Go back multiple steps:

navigate(-2)

With state:

navigate('/profile', { state: { from: 'settings' } })

Rules of Hooks Apply

Must call at top level:

// ✅ CORRECT
function Component() {
  const navigate = useNavigate()

  if (condition) {
    // Can use navigate() here
    return <button onClick={() => navigate('/')}>Home</button>
  }
}

// ❌ WRONG
function Component() {
  if (condition) {
    const navigate = useNavigate() // Can't call hook conditionally
  }
}

Call outside conditions: Hook call must be unconditional.

Use returned function anywhere: The navigate() function can be used in conditions.

When to Use useNavigate

Use useNavigate when:

  • Navigating after async operations
  • Inside event handlers with logic
  • In useEffect for side effect-based navigation
  • After form submission
  • Following mutations/API calls
  • Complex navigation logic

Don't use useNavigate when:

  • Simple link navigation (use <Link>)
  • Conditional redirect during render (use <Navigate>)

Last resort: Prefer <Link> and <Navigate> when possible.

Accessibility Concerns

Links should be <Link> components:

  • Renders semantic <a> tag
  • Built-in accessibility features
  • Right-click context menu support
  • Open in new tab support
  • Browser back/forward buttons work
  • Screen reader friendly

Don't use buttons for navigation:

// ❌ BAD (for simple links)
<button onClick={() => navigate("/about")}>About</button>

// ✅ GOOD
<Link to="/about">About</Link>

Exception: When button performs logic before navigation.

Preserve expected browser behavior: Links should look and act like links.

Usage Examples

After form submission:

import { useNavigate } from 'react-router'

function CreatePost() {
  const navigate = useNavigate()

  async function handleSubmit(event) {
    event.preventDefault()

    const response = await fetch('/api/posts', {
      method: 'POST',
      body: JSON.stringify(formData),
    })

    if (response.ok) {
      navigate('/posts')
    }
  }

  return <form onSubmit={handleSubmit}>...</form>
}

With React Query mutation:

import { useMutation } from '@tanstack/react-query'
import { useNavigate } from 'react-router'

function CreateProduct() {
  const navigate = useNavigate()

  const mutation = useMutation({
    mutationFn: (data) =>
      fetch('/api/products', {
        method: 'POST',
        body: JSON.stringify(data),
      }),
    onSuccess: () => {
      navigate('/products')
    },
  })

  return <button onClick={() => mutation.mutate(data)}>Create</button>
}

Logout with cleanup:

function Header(props) {
  const navigate = useNavigate()

  function handleLogout() {
    // Clear authentication
    window.localStorage.removeItem('token')
    window.sessionStorage.clear()

    // Clear cookies (if needed)
    document.cookie.split(';').forEach((c) => {
      document.cookie =
        c.trim().split('=')[0] + '=;expires=Thu, 01 Jan 1970 00:00:00 UTC'
    })

    // Redirect
    navigate('/', { replace: true })
  }

  return <button onClick={handleLogout}>Logout</button>
}

Conditional navigation:

function PaymentForm() {
  const navigate = useNavigate()

  async function handlePayment() {
    const result = await processPayment()

    if (result.success) {
      navigate('/success')
    } else {
      navigate('/failed')
    }
  }

  return <button onClick={handlePayment}>Pay Now</button>
}

Navigation with state:

function ProductList() {
  const navigate = useNavigate()

  function handleProductClick(product) {
    navigate(`/products/${product.id}`, {
      state: { from: 'list', category: product.category },
    })
  }

  return products.map((p) => (
    <button key={p.id} onClick={() => handleProductClick(p)}>
      {p.name}
    </button>
  ))
}

In useEffect:

function Dashboard() {
  const navigate = useNavigate()
  const [user, setUser] = useState(null)

  useEffect(() => {
    fetch('/api/user')
      .then((res) => res.json())
      .then((data) => setUser(data))
      .catch(() => {
        // Redirect to login on error
        navigate('/login', { replace: true })
      })
  }, [navigate])

  if (!user) return <p>Loading...</p>

  return <h1>Dashboard</h1>
}

Going back:

function ProductDetails() {
  const navigate = useNavigate()

  return (
    <div>
      <button onClick={() => navigate(-1)}>← Back</button>
      <h1>Product Details</h1>
    </div>
  )
}

Delete with confirmation:

function DeleteButton(props) {
  const navigate = useNavigate()

  async function handleDelete() {
    if (!confirm('Are you sure?')) {
      return
    }

    await fetch(`/api/products/${props.id}`, { method: 'DELETE' })
    navigate('/products')
  }

  return <button onClick={handleDelete}>Delete</button>
}

Multi-step form:

function MultiStepForm() {
  const navigate = useNavigate()
  const [step, setStep] = useState(1)

  function handleNext() {
    if (step < 3) {
      setStep(step + 1)
    } else {
      // Final step - submit and navigate
      submitForm()
      navigate('/success')
    }
  }

  return <button onClick={handleNext}>Next</button>
}

Replace history entry:

function Login() {
  const navigate = useNavigate()

  async function handleLogin(credentials) {
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(credentials),
    })

    if (response.ok) {
      // Use replace so user can't go back to login
      navigate('/dashboard', { replace: true })
    }
  }

  return <form onSubmit={handleLogin}>...</form>
}

Summary

  • Programmatic navigation: Navigation triggered by code, not user clicks
  • useNavigate hook: Returns function for programmatic navigation
  • Import: import {useNavigate} from "react-router"
  • Usage pattern:
    const navigate = useNavigate()
    navigate('/path')
    
  • Call hook at top level: Follow rules of hooks (unconditional, top of component)
  • Use returned function anywhere: Can call navigate() in conditions, events, effects
  • Common use cases: After async operations, form submissions, logout, mutations, complex logic
  • Navigation options:
    • navigate("/path") - Basic navigation
    • navigate("/path", {replace: true}) - Replace history entry
    • navigate(-1) - Go back
    • navigate(1) - Go forward
    • navigate("/path", {state: data}) - Pass state
  • Last resort: Prefer <Link> and <Navigate> when possible
  • Accessibility: Use <Link> for simple links (semantic HTML, context menus, new tabs)
  • Don't use buttons for simple navigation
  • When to use useNavigate: Events with logic, async operations, effects, after mutations
  • When to use Link: All simple navigation links
  • When to use Navigate: Conditional redirects during render
  • Logout pattern: Clear auth data then navigate with replace: true
  • Form submissions: Submit data then navigate on success
  • Error handling: Navigate to error pages in catch blocks
  • Preserves SPA experience with proper browser behavior
  • Essential for complex navigation flows in React applications

Summary

URL Parameters:

  • Denoted by colon (:) followed by name
  • Example: /products/:id
  • useParams() hook reads parameters from current route
  • Parameters always returned as strings

Navigate Component:

  • Redirects user to another route
  • replace prop optional but recommended for redirections
  • Prevents user going back after redirect

useNavigate Hook:

  • For programmatic navigation when can't return JSX
  • Returns function to call for navigation
  • Use only when <Link /> or <Navigate /> not possible

Best Practices:

  • Use <Link /> component for all links
  • Use <Navigate /> for conditional redirects during render
  • Use useNavigate() hook for navigation in events/effects
  • Prefer declarative over programmatic when possible