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
.jsxfile extensions by default - There is no functional difference between
.jsand.jsxin 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
reactpackage 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:
idstyleclassName
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 withdocument.createElement()document.createElement(tagName)creates an HTML elementelement.className = "text--regular color--primary"sets classes on the elementclassNameis used instead ofclassbecauseclassis 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()parallelsdocument.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
- Import
Reactobject andcreateRootfunction - Create React element using
React.createElement - Select root DOM element with
document.querySelector() - Call
createRoot(root)to specify render location - Call
.render(element)to render the element - 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-tutorialfolder - 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:
- Open Settings (
cmd + ,on Mac, orCtrl + Shift + P→ "Open Settings UI") - Search for "Format on save"
- 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:
imgbr(line break)hr(horizontal ruler)inputlinkmeta- 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:
bris a void element tag and must neither havechildrennor usedangerouslySetInnerHTML.
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:
FooterChatMessageButton
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:
- Check first character of JSX tag
- Uppercase letter → Component
- 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/constvariable declarationsimport/exportstatementsreturnstatements
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
returnand JSX triggers ASI - ASI inserts semicolon, causing
undefinedreturn - 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
returnand JSX triggers ASI (Automatic Semicolon Insertion) - ASI inserts semicolon, causing
undefinedreturn - 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 defaultfor 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:
- Import
StrictModefrom"react" - 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.logstatements 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 defaultfor 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 + 4→7 - 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:
<Welcome name="Sam" />converts toReact.createElement(Welcome, {name: "Sam"})- React calls
Welcomefunction with props object{name: "Sam"} - Access props through the
propsparameter
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
ifstatements for conditional JSX rendering based on props - JSX converts to
React.createElementcalls, 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
ifstatements to conditionally render JSX based on props - JSX converts to
React.createElementcalls, enabling JavaScript logic
TypeScript Note:
- React deprecated PropTypes
- Modern approach uses TypeScript for prop type checking
- For ESLint, disable
react/prop-typesrule ineslint.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:
- Must be arrow function
- Single statement only (remove curly braces)
- Remove
returnkeyword
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
returnkeyword - JSX elements can be assigned to variables
- Each list item requires
keyprop (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
{}andreturn)
The key Prop:
- React requires
keyprop 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
classNamefor 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
propsobject 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} />→notificationsisnull0:<WelcomeUser notifications={0} />→notificationsis0- Empty string:
<WelcomeUser notifications="" />→notificationsis""
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:
onClickprop (capital C)- Curly braces
{}for JSX expression - 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:
onClickexpects 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
onClickprop adds click event handlers to JSX elements- Must use capital C:
onClicknotonclick - Pass function definition:
onClick={() => code} - Missing
() =>causes immediate execution during render - Use inline arrow functions for simple one-line actions
- Only use
onClickon<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}notonClick={handleClick()} - Adding
()calls function immediately during render - Use
handleEvent,handleSubject, orhandleSubjectEventnaming 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:
onClickprop with function definition - Use
onClickonly onbuttonelements (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}notonClick={handleClick()}
Other Event Props:
onChange- for input, textarea, select elementsonInput- for input, textarea, select elementsonKeyDown,onKeyUp,onKeyPress- keyboard eventsonSubmit- for form elements- Event names follow camelCase:
onKeyDownnotonkeydown
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 stateuseEffect- run side effectsuseRef- reference DOM elementsuseId- generate unique IDsuseContext- access contextuseMemo- memoize valuesuseCallback- 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
useStatemanages state (most common hook)useRefaccesses DOM elementsuseEffectruns code after render (escape hatch)- All hooks start with
useprefix - 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 meanhtmlFor?
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
forattribute for accessibility - JSX uses
htmlForinstead offor(reserved keyword) - Hardcoded IDs break when component reused
useIdgenerates 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}andid={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:
- You might have mismatching versions of React and the renderer (such as React DOM)
- You might be breaking the Rules of Hooks
- 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
useprefix
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/selectneeds relatedlabelelement forattribute in HTML becomeshtmlForin 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:
- Only call hooks from React functions
- 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 →
latvariable - Second array item →
lngvariable
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 foruseStatehook - Destructuring doesn't modify original array
- Position matters: first variable gets first item, second gets second item
- Prepares you for
useStatesyntax: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:
useState(0)called with initial value0- Returns array:
[currentValue, updaterFunction] - Array destructuring:
[counter, setCounter] counteris the state variable (starts at0)setCounteris 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
useStatecreates 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
undefinedif 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
setStatefunction
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:
- Component called,
useState(0)returns[0, setCounter] counteris0- JSX rendered:
<p>The counter is 0</p>
User clicks button:
handleClickcalledsetCounter(counter + 1)→setCounter(0 + 1)→setCounter(1)- React detects state change
Second render:
- Component called again
useState(0)returns[1, setCounter](initial value ignored)counteris now1- JSX rendered:
<p>The counter is 1</p> - React compares new JSX to previous
- React efficiently updates only what changed in DOM
Third render (user clicks again):
setCounter(1 + 1)→setCounter(2)- Component re-renders with
counter = 2 - 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 withcounter = 1 - Click: calls
setCounter(2), re-renders withcounter = 2 - And so on...
React's Reconciliation
React doesn't re-render entire page on state change:
- Component re-runs, generates new JSX
- React compares new JSX to previous (virtual DOM diffing)
- Only changed elements updated in actual DOM
- 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
setStatecalled- Triggers re-render
- Component renders again
setStatecalled 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
setStatefunction to update state, never direct modification - Direct modification doesn't trigger re-renders
setStatehooks into React internals, triggers component re-render- On re-render,
useStatereturns updated value (ignores initial value) - React compares new JSX to previous, updates only what changed
- This reconciliation process is extremely efficient
- Only call
setStateinside event handlers (for now) - Calling
setStateat 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:
stateNameandsetStateNamein camelCase - Must call at top level, only in React components
- Accepts initial value for first render
State Updates:
- Use
setStatefunction to update state (never modify directly) - Calling
setStatetriggers 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? | Location | Example |
|---|---|---|
| ❌ NO | useState() hook call | Never wrap with if |
| ✅ YES | setState() function call | Can 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 levelsetState()function can be conditional inside event handlers- Use
ifconditions 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:
handleClickdefined insideCounterfunctionhandleClickcan accessCounter's variables (counter,setCounter)- Access persists even when
handleClickcalled 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
useStatehook withifcondition (violates rules of hooks) - Can wrap
setStatefunction withifcondition
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
- Rules of Hooks (covered in chapter 17)
- State is immutable (5 dedicated chapters)
- Props are immutable (this lesson)
- 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:
- React throws error when modifying props
- Even if no error, React won't re-render
- 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
| Feature | Props | State |
|---|---|---|
| Mutable | ❌ No | ✅ Yes |
| Triggers re-render | ❌ No | ✅ Yes |
| Owned by | Parent | Component |
| Updated by | Parent | setState |
| Use for | Passing data down | Tracking 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:
- Performance: React can skip unchanged components
- Predictability: Same inputs = same output
- Easier testing: No hidden dependencies
- Concurrency-safe: Parallel rendering works correctly
StrictMode Detects Impurity
React's <StrictMode /> helps catch impure components:
How it works:
- First render (mount component)
- Unmount component
- 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
useStatefor 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:
- Return same output for same input
- 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:
useEffecthook 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
setStatefor 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
useStateworks with any data type (boolean, string, number, etc.)- Boolean state: use
!operator to flip values - Toggle pattern:
setState(!state) - Naming:
isActive,hasError,canEditfor 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
useStatesyntax 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": Onlyh1rendered - When
userType === "admin": Bothh1andprendered
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&& ifstatements: 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) 0is falsy but rendered by JSX - usecount > 0, not justcount- 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)oruseState(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 attrue)count(number, starts at0)
Two independent setters:
setEnabledupdates onlyenabledsetCountupdates onlycount
How React Tracks Multiple States
First render:
- First
useState(true)→ returns[true, setEnabled] - Second
useState(0)→ returns[0, setCount]
Second render (after update):
- First
useState(true)→ returns[false, setEnabled](initial value ignored) - 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:
- First:
useState(0)→count = 0 - After
setCount(5):useState(0)→count = 5(ignores 0) - 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
useStatemultiple 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
useStatecalls 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:
- Duplicated data (
fullNameduplicates first + last) - Duplicated logic (manually keeping
fullNamein sync) - Extra re-renders (3 setState calls instead of 2)
- More complex code (more states to manage)
- 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:
- Single source of truth (
firstNameandlastName) - No manual synchronization
- Fewer re-renders (2 setState calls, not 3)
- Simpler code
- No risk of data inconsistency
Why It Works
Every render executes component function:
firstNameandlastNamehave current valuesfullNamecomputed from current values- 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
letandiffor 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
useStatemultiple times in component - React tracks state values by order of
useStatecalls - 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:
counteris0throughout entire function executionsetCounterschedules 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:
setFirstName("Sam")called → React schedules re-render- Function continues executing
setLastName("Blue")called → React schedules another re-render- Function completes
- 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)
setStateschedules update for next render- State is snapshot - constant throughout render
- Store new value in variable if needed:
const next = state + 1 - Multiple
setStatecalls 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:
- Start:
count = 0 - User clicks button
setCount(count + 1)→ schedules update to1(butcountstill0)setCount(count + 1)→ schedules update to1(still using stalecount = 0)- 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 + 1is short for:
function(c) {
return c + 1;
}
How it works:
-
First
setCount(c => c + 1):- React calls function with current value:
c = 0 - Returns
0 + 1 = 1 - Next state:
1
- React calls function with current value:
-
Second
setCount(c => c + 1):- React calls function with latest value:
c = 1 - Returns
1 + 1 = 2 - Next state:
2
- React calls function with latest value:
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) orcurrentprefix - 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 asnew Array(){}same asnew 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 = firstArraycreates 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
| Type | Comparison | Assignment |
|---|---|---|
| Primitives | By value | By value (copy) |
| Objects/Arrays | By reference | By 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 tonew Array() === new Array(){} === {}equivalent tonew Object() === new Object()- Each literal/constructor creates new instance
- Assignment creates reference, not copy
const b = a(whereais object/array) makesbpoint 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
5to become6 - 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, butObject.is(NaN, NaN)→ true0 === -0→ true, butObject.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
[] === []isfalse(different references){} === {}isfalse(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]:
[]creates new array...array1spreads items fromarray1- Items copied into new array
- 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:
- Create new array
- Spread existing items:
10 - Add new item:
20 - 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:
- Create new array
- Add new item:
5 - Spread existing items:
10, 15 - 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:
- Initial:
values = [] - Click:
setValues([...[], 5])→setValues([5]) - Re-render:
values = [5] - Click:
setValues([...[ 5], 5])→setValues([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
itemsunchanged
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:
| Code | Result | Explanation |
|---|---|---|
items.slice(1) | All except first | Start at index 1, no end specified |
items.slice(2) | All except first two | Start at index 2, no end specified |
items.slice(0, 1) | Only first item | Index 0, stop before index 1 |
items.slice(0, 2) | First two items | Index 0-1, stop before index 2 |
items.slice(0, -1) | All except last | Index 0, stop before last index |
items.slice(0, -2) | All except last two | Index 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}:
- Create new object
- Spread existing key/value pairs
- 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:
- Initial:
user = {id: 1, name: "Sam"} - Click:
setUser({...user, age: 20}) - Creates:
{id: 1, name: "Sam, age: 20} - 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:
- Spread existing:
id: 1, name: "Sam", age: 20 - Calculate new age:
user.age + 1→21 - Override:
age: 21 - 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 propertiesrest- new object withidandtitleonly
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:
- Destructure properties to remove:
id,type - Capture remaining properties:
...rest restbecomes new object with onlynameandage- Pass
resttosetUser()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:
.map()iterates through each object- Find target:
item.name === "Carrot" - Return new object:
{...item, name: "Peas"} - Spread operator creates copy with new name
- 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(likelyundefined) - 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:
- Click "Delete" next to "Sam"
- Arrow function executes
- Calls
handleDelete(1) - Filters out user with
id: 1 - 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
- Identify common ancestor: Find first parent of all components needing state
- Move state up: Move
useState()call to ancestor - Pass down as props: Send state and setter functions as props
- 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) vshandleIncrement(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} />
</>
)
}
Counterreceives:counterandonIncrementSidebarreceives: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})vsfunction 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 submittednew FormData(event.target): Collects all inputs withnameattributesdata.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
useIdhook to generate unique IDs for label-input associations - Add
onSubmithandler 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
nameattribute - Use FormData API:
new FormData(event.target) - Access values:
data.get("inputName") event.targetreferences 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:
- State:
const [username, setUsername] = useState(""); - Value prop:
value={username}- input displays state value - onChange handler:
onChange={e => setUsername(e.target.value)}- updates state when user types
How It Works
Flow:
- User types character
onChangefiressetUsername(event.target.value)called- State updates to new value
- Re-render with new
value={username} - Input displays new value
Always synchronized: username state = input's current value.
event.target.value
What it means:
event: The change event objectevent.target: The input element that fired eventevent.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 +
valueprop +onChangehandler - Syntax:
value={state}andonChange={e => setState(e.target.value)} eis shorthand forevente.target: The input elemente.target.value: Current input value- Benefits: Real-time access, validation, manipulation, conditional rendering
- Drawback: More verbose code, re-renders on every change
- Missing
onChangemakes input read-only (warning) - Use
defaultValuefor 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
onSubmitevent 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
nameattribute - Read values:
data.get("name")
Controlled Inputs:
- Create state variable for input value
- Set both
valueandonChangeprops valueprop withoutonChangemakes input read-only- Allows real-time validation, manipulation, and reactions
- More verbose code than uncontrolled inputs
Uncontrolled Inputs:
- Use
defaultValueprop for default initial value - No state synchronization required
- Simpler code
Event Object:
- React event object differs from native JavaScript event
- Has
target,currentTarget,typeproperties - 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
- React encounters
<Suspense>component - Checks if any child is waiting for promise to complete
- If promise pending: Renders fallback
- If promise fulfilled: Renders children
- 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:
- App renders
- Encounters
<LazySupport /> - React dynamically imports
./Support - Suspense shows fallback during import
- 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 importimport('./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
lazyfrom"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 objectprops.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-boundarypackage (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
FallbackComponentprop, receivesprops.error props.error.messagecontains 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
ErrorBoundaryfromreact-error-boundarypackage - Recommended: Wrap whole application
- Two options:
fallbackprop - generic error messagefallbackComponentprop - 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
methodproperty:fetch(url, {method: "POST"}) - Send data with
bodyproperty - 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
bodyproperty 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:
- Side effect: Violates React's purity requirement
- StrictMode: Renders twice → two fetch calls
- Infinite loops: Combining with
useStatecan cause endless fetches - 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
| Library | Size |
|---|---|
| SWR | 11KB |
| React Query | 62KB |
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,
fetchwith 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
useEffectworks 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:
QueryClientProvidercomponent wrapping appQueryClientinstance 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:
- Import
QueryClientandQueryClientProvider - Create instance:
const queryClient = new QueryClient() - 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-queryinstead - Setup: One-time configuration at app root
- Import
QueryClientandQueryClientProvider - Create instance:
const queryClient = new QueryClient() - Wrap app:
<QueryClientProvider client={queryClient}><App /></QueryClientProvider> - QueryClient manages caching, fetching, mutations, configuration
- Place in
main.jsx(Vite) orindex.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 identifierqueryFn: 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 dataerror: Error if request failedisLoading: 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:
- App renders
- Encounters
<Users /> - Users calls
useSuspenseQuery - Component suspends (pauses)
- Suspense shows fallback
- 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
useSuspenseQueryfrom@tanstack/react-query - Compatible with
<Suspense>component - Requires object with
queryKeyandqueryFn - 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
fetchdirectly (performs side effect) - React specialized in view, doesn't offer fetch solution
- Use libraries:
swror@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
QueryClientProviderwraps entire App- Enables React Query in any child component
- Requires
clientprop: instance ofQueryClient - QueryClient: central manager for caching, fetching, mutation logic
useSuspenseQuery Hook:
- Makes fetch requests in React Query
- Requires object with
queryKeyandqueryFn queryKey: unique identifier for specific queryqueryFn: 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 identifierqueryFn: 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 dataerror: Error if request failedisLoading: 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
AppSetupcomponent - AppSetup wraps App with providers (StrictMode, QueryClientProvider, ErrorBoundary)
- Important: Keep
queryClientoutside components (create once) - Simplified render:
createRoot().render(<AppSetup />) - Repetitive fetch logic → Create
fetcher.jsxfile - Define
BASE_URLand 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 minute5 * 60 * 1000: 5 minutesInfinity: 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
falseor1for 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
AppSetupcomponent wrapping<App /> - Refactored fetch into separate file to avoid repetition
- Created
get(endpoint)function infetcher.jsx
JavaScript Destructuring:
- Can rename destructured properties
- Pattern:
const {oldName: newName} = object
React Query Configuration:
- Aggressive defaults customizable
staleTime: how long data considered freshrefetchOnWindowFocus: refetch when window focusedretry: retry failed queries (and how many times)- Provide defaults to
new QueryClient()call
React Query Caching:
- Automatically caches API responses
- Pass multiple values to
queryKeyfor separate cache entries - Pattern:
queryKey: ['products', userId]vsqueryKey: ['products']
Summary
Project X & React Query Mutations concepts:
- JSX arrays, components, React Query fetching, Suspense integration
useMutationhook for POST/PUT/DELETE requestsmutation.mutate()execution with dynamic datamutationFnwith data parameter,onSuccesscallback- 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
useSuspenseQueryoruseQuery - 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 mutationmutationFn: Function that makes fetch requestdataparameter: Receives data passed tomutate()- 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: handleClick → mutation.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:
- Click button
handleClickcallsmutation.mutate({grade: 15})- Data object passed to
mutationFnasdataparameter - 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
dataparameter - Execute:
mutation.mutate(data)- manual trigger - Pass data:
mutation.mutate({key: value})→ available asdatain 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
useMutationhook to define mutations
useMutation Hook:
- Accepts object with
mutationFnproperty mutationFn: function making fetch request- Receives
dataobject passed frommutation.mutate()
Triggering Mutations:
mutation.mutate({...}): triggers fetch request- Passes data to
mutationFn
Success Handling:
onSuccessproperty: 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 infetcher.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:
- Component renders
<h1>Home</h1>appears in DOM- After rendering, effect executes
logAnalytics("home")called- 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:
- React calculates JSX
- DOM updates
- Browser paints screen
- 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
useEffecthook 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:
- Render component
- Done
React + StrictMode:
- Render component
- Remove it from DOM
- 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:
- Only call from React functions (components, custom hooks)
- 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:
- Event handlers:
handleClick()- user-triggered side effects - useEffect: After rendering - automatic side effects
Forbidden location: Rendering phase (component body).
Prefer Events Over Effects
Priority order:
- Try event handlers first: User interaction triggers side effects
- 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:
"rendering"- Component body executes"effect"- useEffect runs after render"click"- When user clicks button (repeats on each click)
Order: Rendering → Effect → Event handlers (on user input).
With StrictMode (Development)
Output:
"rendering"- First render"rendering"- Second render (StrictMode double-render)"effect"- First effect"effect"- Second effect"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:
- Rendering phase: React calls component function, generates JSX
- DOM update: JSX committed to DOM
- Effect execution: useEffect callbacks run
- 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:
"Rendering phase""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
ifcondition 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 spreadparticleCount: Number of confetti piecesorigin: 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 screendisableForReducedMotion: 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:
handleClickexecutessetCount(count + 1)called- State updates
- Component re-renders
- useEffect runs again
- 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:
- Component renders
- useEffect runs →
setCount(count + 1) - State changes
- Component re-renders (back to step 2)
- 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:
setStateinuseEffectoften 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
setStateinside 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:
callbackFn: Effect function (what to run)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:
- After component renders (first time)
- 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:
- Component renders
- Effect runs (confetti fires)
- User clicks button
- State updates (
count: 1) - Component re-renders
- 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 []:
- Component renders
- Effect runs
- Component removed (StrictMode)
- Cleanup runs (if provided)
- Component renders again
- 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:
- Initial render →
count: 0 - Effect runs → confetti fires
- Button click →
count: 1 - Re-render → JSX updates
- 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:
- Component renders → Effect runs → Logs "Synchronizing with count"
- Button click →
countchanges → Effect re-runs → Logs again - Every click →
countchanges → 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" →
countchanges → Effect re-runs → Logs - Click "Toggle" →
isEnabledchanges → 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:
countchangesisEnabledchanges- 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:
- Button click →
valueupdates Counterreceives newcounterprop- Prop change triggers effect re-run
- 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-synchronizeundefinedor 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):
- Render component (mount)
- Remove component (unmount)
- Render component again (mount)
Effect behavior:
- Effect starts (first render)
- Effect stops (component removed)
- 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:
- Save interval ID:
const intervalId = setInterval(...) - 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:
- Component unmounts (removed from DOM)
- Before effect re-runs (re-synchronization)
Purpose: Stop previous effect before starting new one.
StrictMode Flow with Cleanup
Complete flow:
<Timer />rendered (mounted)- Effect starts → interval scheduled (every 1,000ms)
- React removes component (unmount) → cleanup runs → interval cancelled
<Timer />rendered again (mounted)- 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
setIntervalcalls 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:
- Component unmounts (removed from DOM)
- 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:
- Define named function inside useEffect
- Add event listener with function reference
- Return cleanup function that removes listener
- 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:
- Component mounts → event listener added
- Component unmounts → cleanup runs → listener removed
- 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:
- Component renders → confetti fires → element removed immediately
- Component renders again → confetti fires (this one visible)
No visual difference: First confetti removed before seen.
Memory leak effects (need cleanup):
.addEventListener()- listeners persistsetTimeout- timers persistsetInterval- 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:
- Effect runs → timeout scheduled
- Count changes → cleanup runs (cancels previous timeout) → new timeout scheduled
- 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:
- Define function inside useEffect
- addEventListener with function reference
- 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-queryorSWR - 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:
- User clicks "Focus" button
handleFocusexecutesinputRef.current.focus()called- 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
currentproperty - 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.currentisnull(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
currentkey: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 withref={}→ focus inuseEffect(..., [])
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
currentalways 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:
- Create the context
- Wrap components with context
- 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
createContextfrom "react" - Use PascalCase naming (required for JSX usage)
- Step 2 - Wrap:
<YourContext value={{...}}>children</YourContext> - Wrap components that need access
- Pass data via
valueprop - React 19: No
.Providerneeded (simplified API) - Step 3 - Consume:
const context = useContext(YourContext) - Import
useContexthook - 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:
- Create:
const YourContext = createContext(defaultValue); - Wrap:
<YourContext value={{...}}>...</YourContext> - 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:
- Import
createContext(move from index.jsx) - Add
exportkeyword beforeconst UserContext - 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:
- Remove
createContextimport - Add
import {UserContext} from "./UserContext"(named import) - Keep
useContextimport
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.jsxfile - Step 2: Move context creation with
exportkeyword - 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
createContextimport from component file - Keep
useContextimport 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:
- Import
UserProvider(named export) - Replace
<UserContext value={...}>with<UserProvider> - Remove
namestate 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.childrenwith<YourContext> - props.children: Contains nested components to render
- Step 4: Add
useStatein provider - Step 5: Create functions, pass to
valueprop - 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
exportcontext 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
CartContextfor SuperM app - Provides:
cartstate,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 valuesUserContext- current user/authenticationAppSettingsContext- 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:
pathandelement
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 />notAbout - 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/aboutelement={<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) andelement(JSX component) elementmust be JSX:<About />notAbout- 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:
toprop: Destination path (likehrefin<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>© 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>© 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> toprop: Destination path (likehrefin<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 APIRoutes: decides which Route to render based on URLRoute: declarative route definition withpathandelementprops- Route must be direct child of Routes
elementprop expects JSX component (<Landing />) not name (Landing)
Navigation:
Linkcomponent for navigation withtoprop- Must use Link (not regular anchor tags)
- Generates anchor using history API (no full page reload)
toprop must match route pathnavelement: 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/1→id="1"/products/42→id="42"/products/abc→id="abc"/products/shoe-123→id="shoe-123"/products/anything→id="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 :idname 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/1→idis"1"(string, not number)- Must convert manually:
Number(id)orparseInt(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
userprop 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
useNavigatehook instead) - Inside useEffect (use
useNavigatehook) - After async operations (use
useNavigatehook)
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 /> toprop: Destination route pathreplaceprop: Replaces history entry (prevents back navigation)- Recommended: Use
replacefor redirections - Without
replace: User can use back button to return - Conditional redirects: Use
ifstatements 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
useNavigatehook) - Navigate is declarative: Returns JSX during render
- Multiple conditions: Chain multiple
ifstatements 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 levelnavigate()function stored in variablehandleLogoutclears 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 navigationnavigate("/path", {replace: true})- Replace history entrynavigate(-1)- Go backnavigate(1)- Go forwardnavigate("/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
replaceprop 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