Why React & Core Mental Model
Discover what React is, why it became the world's most dominant front-end JavaScript library, how its declarative paradigm differs from traditional DOM manipulation, and how the Virtual DOM optimizes UI rendering.
- The fundamental problem React was created to solve
- Declarative vs. Imperative programming paradigm
- Component-based architecture and composability
- How the Virtual DOM and Reconciliation diffing work under the hood
- Single Page Applications (SPAs) vs. traditional multi-page websites
01. What is React?
React is a free, open-source JavaScript library developed by Facebook (Meta) in 2013 for building rich, interactive user interfaces. Unlike heavyweight monolithic frameworks, React focuses primarily on the View layer of an application, making it lightweight, flexible, and capable of integrating with any backend or ecosystem.
Before React, web applications relied heavily on direct DOM manipulation using Vanilla JavaScript or jQuery:
// The Old Imperative Way: You tell the browser step-by-step HOW to update const btn = document.getElementById('submit-btn'); const status = document.getElementById('status-msg'); btn.addEventListener('click', () => { btn.disabled = true; status.innerText = 'Sending request...'; status.className = 'loading'; fetchData().then(() => { btn.disabled = false; status.innerText = 'Success!'; status.className = 'success'; }); });
As applications grew in complexity with dozens of widgets, buttons, notifications, and forms, manually updating every element, managing edge cases, and keeping the UI in sync with backend data became a breeding ground for synchronization bugs (spaghetti code).
02. The Declarative Paradigm
React revolutionised front-end development by introducing the declarative mental model: you declare WHAT the user interface should look like for a given state, and React handles HOW to update the browser DOM automatically.
In React, your user interface is purely a function of state:
UI = f(State)
Whenever your state changes, React runs the function again and updates the screen. You never have to manually query or modify DOM nodes.
03. Component-Based Architecture
React structures entire applications as a tree of independent, self-contained, and reusable building blocks called Components. Each component encapsulates its own HTML markup, styling logic, and internal state.
Reusability
Build a component once (e.g. Button, Card, Navbar) and reuse it across multiple screens with different props.
Maintainability
Isolate bugs to a specific component without worrying about unintended side effects across unrelated UI sections.
Composability
Combine small, focused components together like LEGO bricks to assemble massive, feature-rich web platforms.
04. The Virtual DOM & Reconciliation
Directly mutating the browser's Real DOM is computationally expensive. Whenever a DOM element changes, the browser must recalculate page geometry, recompute CSS styles, and perform layout repaints.
React solves this performance bottleneck using an in-memory representation called the Virtual DOM:
- Virtual DOM Snapshot: When a component's state changes, React creates an in-memory Virtual DOM tree representing the new UI.
- Diffing (Reconciliation): React compares the new Virtual DOM tree with the previous Virtual DOM snapshot using a fast diffing algorithm.
- Batch Patching: React computes the minimal set of changes necessary and updates only those specific nodes in the real browser DOM in a single optimized batch.
Why does React use an in-memory Virtual DOM instead of updating the real DOM directly?
Updating the real browser DOM triggers costly layout calculations and repaints. React's Virtual DOM diffs changes in memory first and applies only the exact surgical changes required.
Objective: Analyze the difference between imperative DOM scripting and declarative state-driven rendering.
- Write down 3 real-world UI components you use daily (e.g., an Instagram Post, a Spotify Player, an Amazon Cart).
- For each component, identify what its State is (e.g.,
isLiked,isPlaying,itemCount). - Write down how the visual appearance changes when that state flips between true and false. Notice how you only care about the result, not the step-by-step DOM steps!
Vite Setup & Project Anatomy
Learn how modern React applications are bootstrapped using Vite—the lightning-fast build tool powered by native ES modules—and understand every single folder and file in a production-ready React codebase.
- Why modern development uses Vite instead of deprecated Create React App (CRA)
- Step-by-step commands to scaffold a new React project
- Anatomy of
index.html,main.jsx,App.jsx, andpackage.json - Running the local development server and HMR (Hot Module Replacement)
- Installing and using React Developer Tools in your browser
01. Why Vite?
For years, beginners started with Create React App (CRA). However, CRA is now deprecated by the React team because its Webpack bundler rebuilt the entire application bundle on every code change, causing slow startup times and sluggish reloads in medium-to-large projects.
Vite (French for "quick", pronounced /veet/) leverages native ES modules (ESM) supported by modern browsers and the ultra-fast Go-based compiler esbuild. Vite provides instant server start, lightning-fast Hot Module Replacement (HMR), and optimized production rollups.
02. Creating Your First React App
Ensure you have Node.js (v18 or higher) installed on your machine. Then open your terminal and run:
# 1. Create a fresh React project with Vite npm create vite@latest my-react-app -- --template react # 2. Enter the new directory cd my-react-app # 3. Install required node dependencies npm install # 4. Launch the local dev server npm run dev
Your terminal will output a local URL (e.g. http://localhost:5173). Open it in your web browser to see your live React app running!
03. Project Anatomy: Breaking Down Every File
Let us explore the created files and their responsibilities:
my-react-app/ ├── node_modules/ # Installed third-party packages ├── public/ # Static assets (favicon, images, logos) ├── src/ # Where 99% of your React application code lives │ ├── assets/ # Images/SVGs imported directly into components │ ├── App.css # Styles specific to the root App component │ ├── App.jsx # Main root application component │ ├── index.css # Global baseline CSS styles │ └── main.jsx # Application entry point: mounts React to DOM ├── index.html # The single HTML page loaded by the browser ├── package.json # Dependencies, scripts, and project metadata └── vite.config.js # Vite build and plugin configurations
04. The Mounting Process: index.html to main.jsx
How does React take over the web page? It starts in index.html with a single container div:
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>My React App</title> </head> <body> <!-- React will mount inside this empty container --> <div id="root"></div> <!-- Entry script that boots React --> <script type="module" src="/src/main.jsx"></script> </body> </html>
Next, src/main.jsx grabs that #root element using ReactDOM.createRoot() and renders the top-level <App /> component inside it:
import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.jsx'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')).render( <React.StrictMode> <App /> </React.StrictMode> );
<React.StrictMode> is a development-only helper tool that highlights potential bugs, deprecated APIs, and accidental side effects by intentionally executing components twice in local dev. It has zero performance impact on production builds.
Where does the primary React application mount into the browser HTML?
ReactDOM.createRoot selects the HTML element with id root and renders the top-level <App /> tree inside it.
Objective: Create your first live project and customize the root component.
- Open your terminal and run:
npm create vite@latest hello-react -- --template react - Run
cd hello-react && npm install && npm run dev - Open
src/App.jsxin your editor, delete the default boilerplate, and return a clean<h1>Welcome to React Mastery!</h1>with your name and today's date in a paragraph tag. - Save the file and watch Vite instantly hot-reload the browser without full page refresh!
JSX Syntax & Rendering Rules
Master JSX (JavaScript XML)—the expressive syntax extension that lets you write HTML-like markup directly inside JavaScript files. Understand how expressions evaluate, key differences from standard HTML, and essential formatting rules.
- What JSX is and how Babel/Vite transpiles it into
React.createElement - The curly brace syntax
{expression}for dynamic variables, math, and function calls - Differences between HTML and JSX:
className,htmlFor, camelCase properties - The Single Parent Rule and why React Fragments (
<></>) prevent extra DOM nodes - How booleans, null, and undefined render in JSX
01. What is JSX?
JSX stands for JavaScript XML. It is a syntax extension for JavaScript that allows you to write markup that looks almost identical to HTML directly inside your JS/JSX files.
Browsers cannot understand JSX directly. During your build process, Vite uses Babel or SWC to convert every JSX element into pure JavaScript function calls:
// 1. What you write in JSX: const element = <h1 className="title">Hello React!</h1>; // 2. What it transpiles to behind the scenes: const element = React.createElement( 'h1', { className: 'title' }, 'Hello React!' );
02. Embedding Dynamic JavaScript Expressions
In standard HTML, you write static text. In JSX, you can embed ANY valid JavaScript expression by wrapping it in curly braces {}:
function UserProfile() { const user = { name: 'Sophia Chen', role: 'Full-Stack Engineer', avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330', reputation: 98.4 }; const getGreeting = () => 'Welcome back'; return ( <div className="profile-card"> <img src={user.avatar} alt={user.name} width="64" height="64" /> <h2>{getGreeting()}, {user.name}!</h2> <p>Role: <strong>{user.role.toUpperCase()}</strong></p> <p>Score: {user.reputation * 10} / 1000</p> <p>Next review: {2026 + 1}</p> </div> ); }
03. Crucial Rules & Differences from HTML
| HTML Standard | JSX Equivalent | Reason |
|---|---|---|
class="card" |
className="card" |
class is a reserved keyword in JavaScript for defining classes. |
for="email" |
htmlFor="email" |
for is a reserved keyword in JavaScript for loops. |
onclick="handleClick()" |
onClick={handleClick} |
JSX attributes use camelCase and pass function references, not string scripts. |
style="color: red; font-size: 16px" |
style={{ color: 'red', fontSize: '16px' }} |
Inline styles in JSX must be JavaScript objects with camelCase CSS properties. |
Unclosed tags: <img>, <input>, <br> |
Self-closing: <img />, <input />, <br /> |
JSX follows strict XML syntax rules. Every single opened tag must be closed. |
04. The Single Parent Rule & React Fragments
A React component can only return a single root element. If you try to return two sibling tags side-by-side, transpilation will fail with a syntax error:
// ❌ SYNTAX ERROR: Adjacent JSX elements must be wrapped in an enclosing tag return ( <h1>Header</h1> <p>Description</p> ); // ✅ SOLUTION: Wrap in a React Fragment (<>...</>) without adding useless divs to the DOM! return ( <> <h1>Header</h1> <p>Description</p> </> );
Which attribute name must you use in JSX when adding CSS classes to an element?
Because class is already a reserved keyword in JavaScript for defining ES6 classes, React uses className to assign HTML class names.
Objective: Fix all errors in an invalid HTML snippet to make it valid JSX.
- Given this raw HTML:
<div class="badge"> <img src="avatar.png"> <label for="username" style="font-size: 14px; margin-top: 10px;">Name</label> <input type="text" id="username" tabindex="1"> </div>
- Convert
classtoclassName. - Convert
fortohtmlFor. - Self-close the
<img>and<input>tags. - Convert the string style into a JavaScript object:
style={{ fontSize: '14px', marginTop: '10px' }}. - Convert
tabindexto camelCasetabIndex.
Components & Component Architecture
Learn how to design, organize, and compose React components like a senior engineer. Understand functional components, naming conventions, pure rendering principles, and industry file organization strategies.
- How functional components work and why they replaced Class components
- Why component names MUST start with an uppercase capital letter
- Composing nested component hierarchies (parent-child relationship)
- Writing Pure Components that are free from side effects during render
- Professional project file architectures: feature-based vs type-based
01. Functional Components in Modern React
In modern React, a Component is simply a JavaScript function that accepts input (props) and returns user interface markup (JSX). In the past, developers wrote Class components with render() methods, but since React 16.8 (Hooks), functional components are the official industry standard.
// Modern Functional Component export default function Header() { return ( <header className="site-header"> <div className="logo">CodeTutorium</div> <nav> <a href="#features">Features</a> <a href="#pricing">Pricing</a> </nav> </header> ); }
React component names MUST start with a capital letter (e.g. Header, UserProfile, ProductCard). If you start a component with a lowercase letter (e.g. function header()), React treats it as an ordinary HTML tag (like <div> or <header>) and will not render your component!
02. Component Composition
Instead of cramming hundreds of lines of HTML into a single monolithic App.jsx, senior developers split interfaces into modular components and compose them together:
import Header from './components/Header'; import HeroSection from './components/HeroSection'; import FeatureList from './components/FeatureList'; import Footer from './components/Footer'; export default function App() { return ( <div className="app-shell"> <Header /> <main> <HeroSection /> <FeatureList /> </main> <Footer /> </div> ); }
03. Pure Functions: Keeping Components Idempotent
React relies heavily on the mathematical concept of Pure Functions. A pure function has two characteristics:
- It minds its own business: It does not mutate any variables or objects that existed before the function call.
- Same inputs, same outputs: Given the same inputs, it will always return the exact same JSX result.
// ❌ IMPURE: Mutating external variables during render causes random bugs let visitorCount = 0; function BadCounter() { visitorCount = visitorCount + 1; // Modifies external state during render! return <p>Visitor: {visitorCount}</p>; } // ✅ PURE: Keeps rendering logic isolated and predictable function GoodCounter({ currentCount }) { return <p>Visitor: {currentCount}</p>; }
04. Recommended Directory Organization
src/ ├── components/ # Shared generic UI components (Button, Modal, Input) │ ├── Button/ │ │ ├── Button.jsx │ │ └── Button.module.css │ └── Modal/ ├── features/ # Domain-specific feature modules │ ├── auth/ # LoginForm, RegisterModal, authHooks │ └── products/ # ProductGrid, ProductCard, CartDrawer ├── hooks/ # Reusable custom React hooks ├── utils/ # Helper functions (formatting dates, currency) ├── App.jsx └── main.jsx
What happens if you name a custom React component starting with a lowercase letter, such as function card()?
React uses the initial letter to distinguish native HTML elements (lowercase like div, span) from custom components (uppercase like Card, Navbar).
Objective: Create a modular component hierarchy across multiple files.
- Create a folder
src/components/. - Inside, create
StatCard.jsxthat renders a title, a numerical value, and a percentage badge. - Create
ActivityList.jsxthat renders 3 recent activity items. - Import both into
App.jsxand render three<StatCard />elements inside a grid layout above the<ActivityList />.
Props & Unidirectional Data Flow
Master Props (properties)—the mechanism React uses to pass data, configurations, and callbacks from parent components down to child components. Understand destructuring, default fallback values, and the versatile children prop.
- What props are and how they make components dynamic and customizable
- Unidirectional data flow (one-way data binding from parent to child)
- Clean object destructuring and default fallback parameters
- The special
childrenprop for creating reusable layout wrappers - Passing functions and callbacks as props to communicate back up
01. What are Props?
Think of React components as custom HTML elements. Just as standard HTML elements accept attributes like href, src, and id, React components accept custom attributes called Props (short for properties).
Props allow a single component template to be reused with completely different data:
// Child Component receives "props" as its first parameter export default function CourseCard(props) { return ( <div className="course-card"> <span className="badge">{props.category}</span> <h3>{props.title}</h3> <p>Instructor: {props.instructor}</p> <p>Duration: {props.durationHours} hours</p> <button>Enroll for ${props.price}</button> </div> ); }
Now the parent component passes the exact values it needs:
import CourseCard from './CourseCard'; export default function CourseCatalog() { return ( <section className="catalog-grid"> <CourseCard title="React Mastery" instructor="Sarah Connor" category="Frontend" durationHours={18} price={49.99} /> <CourseCard title="Node.js Backend Architecture" instructor="David Miller" category="Backend" durationHours={24} price={59.99} /> </section> ); }
02. Clean Destructuring & Default Values
Instead of constantly typing props.something, modern JavaScript developers destructure props directly inside the function signature. You can also specify default fallback values:
// Clean destructuring with default fallback values export default function Button({ label, variant = 'primary', size = 'medium', disabled = false, onClick }) { return ( <button className={`btn btn-${variant} btn-${size}`} disabled={disabled} onClick={onClick} > {label} </button> ); }
03. Props are Read-Only (Immutable!)
Props in React are strictly read-only. A child component must never modify the props passed to it:
function BadComponent(props) {
props.title = "New Title"; // ❌ RUNTIME ERROR! Props are frozen/immutable!
}
If a component needs to change data over time in response to user action, that data belongs in State, not props!
04. The Versatile children Prop
Sometimes you want a component to act as a visual container or wrapper (like a Modal dialog, Card, or Layout container) without knowing ahead of time what elements will go inside it. React provides the special children prop for this exact slot pattern:
export default function Card({ title, children }) { return ( <div className="card"> <div className="card-header"> <h3>{title}</h3> </div> <div className="card-body"> {/* Anything nested between <Card>...</Card> renders here! */} {children} </div> </div> ); } // Usage in another component: <Card title="System Notification"> <p>Your storage is 85% full. Consider upgrading.</p> <button className="btn-upgrade">Upgrade Now</button> </Card>
Which of the following statements about React props is TRUE?
Props flow in one direction (unidirectional data flow) and are read-only. Modifying them violates React's pure rendering architecture.
Objective: Build a reusable wrapper component using props and children.
- Create
src/components/Alert.jsx. - It should accept:
type(one of:'success','warning','danger', default to'info'),dismissible(boolean, default false), andchildren. - Render a container with the corresponding CSS class and an icon matching the type.
- If
dismissibleis true, render a close button (×). - In
App.jsx, test all 4 alert types with different custom paragraph text inside!
State Management with the useState Hook
Discover the heart of dynamic React applications: component State. Learn why local JavaScript variables fail to re-render the screen, master the useState hook, understand asynchronous state batching, and handle object/array states properly.
- The difference between Props (external/immutable) and State (internal/mutable)
- The
useStatehook syntax and array destructuring pattern - Why changing regular variables does not trigger a re-render
- The functional updater pattern
setState(prev => prev + 1)to avoid stale state - Safely updating arrays and objects in state using the spread operator (
...)
01. Why Variables Don't Work for Interactive UIs
Suppose you want a simple counter button that increments when clicked. If you try using a regular local variable:
export default function BrokenCounter() { let count = 0; function handleClick() { count = count + 1; console.log('Current count:', count); // Logs 1, 2, 3 in the console... } // ❌ The screen NEVER updates! It stays stuck at 0! return <button onClick={handleClick}>Clicked {count} times</button>; }
Why did this fail? Two critical reasons:
- Local variables don't persist across renders: Whenever React re-executes a component function, local variables are reinitialized from scratch.
- Changing local variables does not notify React: React has no magical listener watching local variables. To trigger a re-render, you must inform React through its official state API.
02. Enter the useState Hook
React provides the useState Hook to store data that persists across renders and triggers an automated UI update when changed:
import { useState } from 'react'; export default function WorkingCounter() { // Syntax: const [stateValue, setterFunction] = useState(initialValue); const [count, setCount] = useState(0); function handleIncrement() { setCount(count + 1); // Updates state and schedules a re-render! } return ( <div className="counter-box"> <h2>Score: {count}</h2> <button onClick={handleIncrement}>Add +1</button> <button onClick={() => setCount(0)}>Reset</button> </div> ); }
03. The Functional Updater Pattern
React batches multiple state updates together for performance. If you need to calculate the next state based on the previous state, always pass an updater callback function rather than relying on the raw state variable:
// ❌ BUGGY: If called 3 times in a row, it only increments once! function incrementThreeTimesBroken() { setCount(count + 1); setCount(count + 1); setCount(count + 1); } // ✅ SAFE & PROFESSIONAL: Uses previous state snapshot guarantee function incrementThreeTimesSafe() { setCount(prev => prev + 1); setCount(prev => prev + 1); setCount(prev => prev + 1); // Correctly increments by +3! }
04. Updating Objects & Arrays in State
Remember: React detects changes by comparing object memory references (shallow equality). If you mutate an existing array or object directly in place, its memory reference stays identical, and React will skip re-rendering!
const [user, setUser] = useState({ name: 'Alex', age: 25, city: 'London' }); const [items, setItems] = useState(['React', 'Vite']); // 1. Updating an Object (Spread existing properties, overwrite target): setUser(prev => ({ ...prev, city: 'Tokyo' })); // 2. Adding to an Array (DO NOT use items.push!): setItems(prev => [...prev, 'TypeScript']); // 3. Removing an item from an Array (use .filter()): setItems(prev => prev.filter(item => item !== 'Vite'));
How should you add a new item to an array stored in React state?
Direct mutation via .push() does not create a new array reference, meaning React will not detect the change. Spreading into a new array ensures state immutability.
Objective: Build a component with multiple interdependent state updates.
- Create a component
QuantitySelector.jsxwith a statequantityinitialized to 1. - Include buttons for:
- 1(cannot go below 1),+ 1, and+ 10. - Display the total price based on a unit price prop (e.g.
unitPrice = 24.99). - Add a discount state: if quantity >= 5, apply a 15% discount and display a green "15% Bulk Discount Applied!" badge.
Handling Events & Synthetic Events
Master user event handling in React. Understand React's SyntheticEvent wrapper, passing custom arguments to handlers, stopping propagation, preventing default browser submissions, and avoiding the common infinite loop trap.
- The SyntheticEvent cross-browser normalization wrapper
- Attaching click, change, submit, and keyboard event handlers
- Passing custom arguments to event handlers cleanly
- Preventing default form submissions with
e.preventDefault() - The critical difference between passing a function reference vs calling it immediately
01. React SyntheticEvents
When you attach an event handler in React (like onClick or onKeyDown), React does not attach raw event listeners directly to the individual DOM nodes. Instead, it wraps native browser events in an object called a SyntheticEvent.
The SyntheticEvent provides a completely identical interface across all browsers (Chrome, Safari, Firefox, Edge, mobile browsers), eliminating cross-browser inconsistencies with event properties.
02. Passing Functions: Reference vs Immediate Invocation
You must always pass a function reference to an event attribute, NOT the result of calling the function immediately:
// ❌ WRONG: Calls handleClick() immediately during rendering! // This triggers an infinite re-render loop if handleClick calls setState! <button onClick={handleClick()}>Click Me</button> // ✅ CORRECT: Passes the function as a reference to be called upon user click <button onClick={handleClick}>Click Me</button> // ✅ CORRECT: Using an inline arrow function when you need to pass custom arguments <button onClick={() => handleDelete(product.id)}>Delete Product</button>
03. Common Event Handlers in Practice
import { useState } from 'react'; export default function EventShowcase() { const [text, setText] = useState(''); const [isHovered, setIsHovered] = useState(false); // Input Change Event function handleInputChange(e) { setText(e.target.value); } // Key Down Event (e.g. detecting Enter key) function handleKeyDown(e) { if (e.key === 'Enter') { alert(`Submitted: ${text}`); } } return ( <div className="events-container"> <input type="text" placeholder="Type and press Enter..." value={text} onChange={handleInputChange} onKeyDown={handleKeyDown} /> <div className={`hover-box ${isHovered ? 'active' : ''}`} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {isHovered ? 'Mouse is inside!' : 'Hover over me'} </div> </div> ); }
04. Stopping Event Propagation & Preventing Defaults
In standard web forms, clicking a submit button reloads the entire page. In Single Page Applications (SPAs), we prevent this using e.preventDefault():
function SearchForm() { function handleSearchSubmit(e) { e.preventDefault(); // Stops the browser from reloading the page! console.log('Performing smooth AJAX client search...'); } return ( <form onSubmit={handleSearchSubmit}> <input type="search" placeholder="Search docs..." /> <button type="submit">Search</button> </form> ); }
What is the result of writing <button onClick={alert('Hello!')}>?
Adding parentheses () calls the function immediately during the rendering phase. You should write onClick={() => alert('Hello!')} to defer execution until the click occurs.
Objective: Handle input, blur, focus, and keyboard events.
- Create a component with a search input and a character counter (e.g. "Characters: 14 / 50").
- When the input is focused, change the border color to cyan. When blurred, revert it.
- If the user presses the Escape key inside the input, immediately clear the text state.
- Add a "Copy to Clipboard" button that uses the browser Clipboard API when clicked.
Conditional Rendering Patterns
Learn the 4 core design patterns for conditionally showing, hiding, and switching user interface elements in React. Master ternary operators, short-circuit logic, guard clauses, and avoid the infamous "0 rendered on screen" bug.
- Pattern 1: The Ternary Operator for binary branching (
if/else) - Pattern 2: Logical AND (
&&) for showing or hiding single elements - The infamous
0 && <Component />falsy number bug and how to prevent it - Pattern 3: Early returns (Guard Clauses) for loading and error states
- Pattern 4: Object map lookup for multi-status states (Tabs, Steps, Enums)
01. Pattern 1: The Ternary Operator
When you have two distinct mutually exclusive UI states (like Logged In vs Logged Out), the JavaScript ternary operator condition ? <TrueComponent /> : <FalseComponent /> is the cleanest inline solution:
export default function AuthBanner({ isLoggedIn, username }) { return ( <div className="auth-bar"> {isLoggedIn ? ( <div className="welcome"> <span>Hello, <strong>{username}</strong>!</span> <button className="btn-logout">Log Out</button> </div> ) : ( <div className="guest"> <span>Welcome, Guest. Please sign in.</span> <button className="btn-login">Sign In</button> </div> )} </div> ); }
02. Pattern 2: Short-Circuit Evaluation (&&)
When you only want to render something when a condition is true, and render NOTHING when it is false, use the logical && operator:
function Inbox({ unreadMessages, hasWarning }) { return ( <div className="inbox"> <h2>Messages</h2> {/* Only renders the red badge if unreadMessages is greater than 0 */} {unreadMessages > 0 && ( <span className="badge-danger">{unreadMessages} new</span> )} {hasWarning && <p className="warning-text">Your inbox is almost full!</p>} </div> ); }
In JavaScript, 0 && <Component /> evaluates to 0 (not false). React renders the number 0 directly onto your page!
// ❌ BUGGY: If count is 0, this renders a stray literal "0" onto the screen! {count && <p>You have items</p>} // ✅ SAFE: Always compare explicitly to a boolean or use greater-than {count > 0 && <p>You have items</p>} {Boolean(count) && <p>You have items</p>}
03. Pattern 3: Early Return (Guard Clauses)
If an entire component should not render or needs to display a full-page loading skeleton or error state, return early before the main JSX:
export default function DataView({ isLoading, error, data }) { // 1. Guard Clause for Loading if (isLoading) { return <div className="spinner">Loading your profile...</div>; } // 2. Guard Clause for Error if (error) { return <div className="error-box">Failed to load: {error.message}</div>; } // 3. Main Happy Path Render return ( <article className="content"> <h1>{data.title}</h1> <p>{data.body}</p> </article> ); }
What will be rendered by the code {items.length && <ItemList />} when items is an empty array ([])?
Since items.length is 0, JavaScript's logical && short-circuits and returns 0. React considers 0 a valid renderable number, printing a stray "0" on the screen.
Objective: Render different component views based on active tab state.
- Create a component with 3 tabs:
Overview,Settings, andBilling. - Store the active tab in state (e.g.
const [tab, setTab] = useState('overview')). - Highlight the active tab button with a CSS class
active. - Use conditional rendering to show the corresponding content panel when clicked.
Rendering Lists & The Critical Key Prop
Master transforming raw JavaScript arrays of data into dynamic lists of React elements using the map() method. Understand why the unique key prop is mandatory for performance, and why using array indexes as keys leads to insidious UI bugs.
- Using
array.map()to transform data collections into JSX elements - What the
keyprop is and how React's reconciliation algorithm uses it - Why using array index as a key breaks state when deleting or sorting items
- Filtering and searching arrays dynamically before mapping
- Handling empty state fallbacks when an array contains 0 items
01. Mapping Arrays to Elements
In React, we do not use for loops inside JSX markup. Instead, we use JavaScript's functional .map() method to project an array of data into an array of JSX elements:
export default function UserDirectory() { const developers = [ { id: 'dev-101', name: 'Sarah Connor', skill: 'React & Next.js' }, { id: 'dev-102', name: 'Kenji Sato', skill: 'TypeScript & Rust' }, { id: 'dev-103', name: 'Amara Okafor', skill: 'UI/UX & CSS' } ]; return ( <div className="directory"> <h2>Engineering Team</h2> <ul> {developers.map(dev => ( <li key={dev.id} className="dev-card"> <strong>{dev.name}</strong> — <span>{dev.skill}</span> </li> ))} </ul> </div> ); }
02. Why React Demands the key Prop
Notice the key={dev.id} attribute on the <li> element above. If you omit it, React will print a red warning in your browser console: "Each child in a list should have a unique 'key' prop."
Why is this key so critical? During reconciliation, when an item is added, moved, sorted, or removed from a list, React compares the new list with the previous list. Without a stable key, React cannot tell which exact item was modified versus which items just shifted position.
03. The Danger of Using Array Index as a Key
Beginners often write items.map((item, index) => <li key={index}>...</li>). This is dangerous if your list is dynamic!
If you delete the first item at index 0, the second item now becomes index 0, the third becomes index 1, and so on. Any internal input values, checkbox checks, or animations attached to index 0 will now mistakenly stay bound to the wrong item!
Rule: Always use unique, persistent IDs (e.g. database UUIDs, item.id, or crypto.randomUUID() when creating items).
04. Filtering Lists with Empty State Handling
import { useState } from 'react'; export default function SearchableProductList({ products }) { const [query, setQuery] = useState(''); // Filter products based on search input const filteredProducts = products.filter(product => product.name.toLowerCase().includes(query.toLowerCase()) ); return ( <div> <input type="search" placeholder="Search products..." value={query} onChange={e => setQuery(e.target.value)} /> {filteredProducts.length === 0 ? ( <p className="empty-msg">No products match your search.</p> ) : ( <div className="grid"> {filteredProducts.map(p => ( <div key={p.id} className="item"> <h4>{p.name}</h4> <p>${p.price}</p> </div> ))} </div> )} </div> ); }
When is it acceptable to use the array index as a key in React?
If the list is strictly read-only and static, index can be used without bugs. If items can be reordered, deleted, or inserted, index keys cause subtle rendering and state bugs.
Objective: Render dynamic lists with add and delete capabilities.
- Initialize state with an array of 3 inventory items:
{ id: '1', name: 'Mechanical Keyboard', qty: 12 }. - Render each item inside a table with columns: ID, Name, Quantity, and an "Actions" column.
- Include a "Delete" button beside each item that filters it out by its unique ID.
- Include a simple form at the top with a text input and number input to add a new product with
crypto.randomUUID()as its ID.
Forms & Controlled Components
Master managing user input in React. Understand the Controlled Component pattern where React state acts as the Single Source of Truth, handle text fields, selects, checkboxes, and manage multi-input forms with a single state object.
- Controlled vs. Uncontrolled input elements
- The two-way binding pattern:
value={state}andonChange={handler} - Managing multi-field forms using dynamic computed property names (
[name]: value) - Handling checkboxes, radio groups, and dropdown selects
- Form validation, submission handling, and error states
01. Controlled vs. Uncontrolled Components
In standard HTML, form elements like <input> and <textarea> maintain their own internal state based on what the user types. This is known in React as an Uncontrolled Component.
In modern React, we prefer Controlled Components: the input's value is driven directly by React state, and changes are piped through state setters. React state is the Single Source of Truth.
import { useState } from 'react'; export default function SimpleNameInput() { const [name, setName] = useState(''); return ( <div> <label htmlFor="full-name">Your Name:</label> <input id="full-name" type="text" value={name} // 1. Push state into input value onChange={e => setName(e.target.value)} // 2. Push input edits back into state /> <p>Preview: {name ? name : 'Anonymous'}</p> </div> ); }
02. Managing Multi-Field Forms with a Single State Object
What if your form has 8 input fields? Creating 8 individual useState calls leads to repetitive boilerplate. Instead, use a single state object with dynamic object keys matching each input's name attribute:
import { useState } from 'react'; export default function RegistrationForm() { const [formData, setFormData] = useState({ email: '', password: '', role: 'student', agreeTerms: false }); const [error, setError] = useState(''); // Single generic change handler for all inputs function handleChange(e) { const { name, value, type, checked } = e.target; setFormData(prev => ({ ...prev, // If it's a checkbox, store its boolean checked state, otherwise store value [name]: type === 'checkbox' ? checked : value })); } function handleSubmit(e) { e.preventDefault(); if (!formData.agreeTerms) { setError('You must agree to the terms to continue.'); return; } setError(''); console.log('Submitted registration data:', formData); } return ( <form onSubmit={handleSubmit} className="form-card"> <h3>Create Free Account</h3> {error && <div className="error-alert">{error}</div>} <div className="field"> <label>Email Address</label> <input type="email" name="email" value={formData.email} onChange={handleChange} required /> </div> <div className="field"> <label>Password</label> <input type="password" name="password" value={formData.password} onChange={handleChange} required /> </div> <div className="field"> <label>Learning Track</label> <select name="role" value={formData.role} onChange={handleChange}> <option value="student">Student / Beginner</option> <option value="developer">Professional Developer</option> <option value="educator">Teacher / Educator</option> </select> </div> <div className="checkbox-field"> <input type="checkbox" name="agreeTerms" id="terms" checked={formData.agreeTerms} onChange={handleChange} /> <label htmlFor="terms">I agree to terms of service</label> </div> <button type="submit" className="btn-primary">Sign Up</button> </form> ); }
What defines a "Controlled Component" in React?
A controlled component takes its current value from React state and calls an onChange handler when user changes occur, keeping React as the single source of truth.
Objective: Build a contact form with client-side feedback.
- Fields: Name, Email, Subject dropdown, Message textarea.
- Real-time validation: if message is under 20 characters, display a warning counter: "Message too short (X / 20 chars)".
- Disable the submit button until all fields are valid.
- Upon submission, display a pleasant success banner and reset all fields back to blank.
🛠️ Project 1: Dynamic Task & Habit Tracker
Put everything learned in Units I and II into action! In this real-world hands-on project, you will build a complete, interactive Task & Habit Tracker featuring item creation, completion toggling, deletion, filtering, and real-time completion statistics.
- State Management: Array of tasks with IDs, titles, categories, and completed flags
- Form Handling: Controlled inputs to add new tasks with validation
- Filters: Live switching between "All", "Pending", and "Completed"
- Progress Bar: Dynamic calculation of completed task percentage
- Empty States: Friendly empty state banners when no tasks exist
01. Complete Project Source Code
Create a file named src/HabitTracker.jsx and study how the state, events, and subcomponents work harmoniously:
import { useState } from 'react'; export default function HabitTracker() { // 1. Main task list state const [tasks, setTasks] = useState([ { id: 't-1', text: 'Complete React Lecture 10', category: 'Study', completed: true }, { id: 't-2', text: 'Drink 2L of water', category: 'Health', completed: false }, { id: 't-3', text: 'Push Git commits to GitHub', category: 'Code', completed: false } ]); // 2. Input states const [inputText, setInputText] = useState(''); const [category, setCategory] = useState('Study'); const [filter, setFilter] = useState('all'); // 'all' | 'pending' | 'completed' // 3. Add new task function handleAddTask(e) { e.preventDefault(); if (!inputText.trim()) return; const newTask = { id: crypto.randomUUID(), text: inputText.trim(), category: category, completed: false }; setTasks(prev => [newTask, ...prev]); setInputText(''); } // 4. Toggle completed flag function handleToggle(id) { setTasks(prev => prev.map(task => task.id === id ? { ...task, completed: !task.completed } : task )); } // 5. Delete task function handleDelete(id) { setTasks(prev => prev.filter(task => task.id !== id)); } // 6. Filter tasks const filteredTasks = tasks.filter(task => { if (filter === 'pending') return !task.completed; if (filter === 'completed') return task.completed; return true; }); // 7. Calculate statistics const totalCount = tasks.length; const completedCount = tasks.filter(t => t.completed).length; const percentage = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0; return ( <div className="tracker-app"> <header className="tracker-header"> <h1>Daily Habit & Task Tracker</h1> <p>Stay consistent. Ship projects.</p> <div className="progress-card"> <div className="progress-stats"> <span>Progress: {completedCount} of {totalCount} completed</span> <strong>{percentage}%</strong> </div> <div className="progress-track"> <div className="progress-fill" style={{ width: `${percentage}%` }}></div> </div> </div> </header> {/* Add Task Form */} <form onSubmit={handleAddTask} className="tracker-form"> <input type="text" placeholder="What habit do you want to achieve today?" value={inputText} onChange={e => setInputText(e.target.value)} required /> <select value={category} onChange={e => setCategory(e.target.value)}> <option value="Study">Study</option> <option value="Code">Code</option> <option value="Health">Health</option> <option value="Work">Work</option> </select> <button type="submit">+ Add Habit</button> </form> {/* Filter Tabs */} <div className="filter-tabs"> <button className={filter === 'all' ? 'active' : ''} onClick={() => setFilter('all')} >All ({totalCount})</button> <button className={filter === 'pending' ? 'active' : ''} onClick={() => setFilter('pending')} >Active ({totalCount - completedCount})</button> <button className={filter === 'completed' ? 'active' : ''} onClick={() => setFilter('completed')} >Done ({completedCount})</button> </div> {/* Task List */} <ul className="task-list"> {filteredTasks.length === 0 ? ( <li className="empty-state">No tasks in this view. Keep up the great work!</li> ) : ( filteredTasks.map(task => ( <li key={task.id} className={`task-item ${task.completed ? 'done' : ''}`}> <label className="checkbox-wrap"> <input type="checkbox" checked={task.completed} onChange={() => handleToggle(task.id)} /> <span className="task-label">{task.text}</span> </label> <span className=`category-badge cat-${task.category.toLowerCase()}`> {task.category} </span> <button onClick={() => handleDelete(task.id)} className="btn-del" title="Delete task" > × </button> </li> )) )} </ul> </div> ); }
Take this project further by adding these 3 advanced features:
- Priority Flag: Add an option to mark tasks as "High Priority" and display a glowing red flame icon (🔥).
- Clear All Completed: Add a button at the bottom to bulk-delete all completed items with a single click.
- Edit Task Text: Add a double-click event on task labels that turns them into editable inline inputs!
The useEffect Hook & Lifecycle Management
Master handling side effects in React. Understand the component lifecycle (Mount, Update, Unmount), master the Dependency Array rules, and write cleanup functions to avoid memory leaks with timers and event listeners.
- What "Side Effects" are in functional programming and React
- The 3 phases of a component: Mounting, Updating, and Unmounting
- The Dependency Array: Empty
[], Dependencies[a, b], and No Array - The Cleanup Function: Stopping intervals, unsubscribing, preventing memory leaks
- How StrictMode interacts with useEffect in development
01. What is a Side Effect?
A pure React component should only receive props and return JSX. However, real-world applications need to interact with the outside world beyond React's rendering pipeline. These operations are called Side Effects:
- Fetching data from an external REST API
- Setting timers (
setInterval,setTimeout) - Directly changing the document title (
document.title = '...') - Listening to global window events (window resize, scroll)
- Reading or writing to browser
localStorage
02. The 3 Dependency Array Modes
The useEffect hook takes two arguments: an effect callback function, and an optional Dependency Array. How and when it executes depends entirely on this array:
import { useEffect, useState } from 'react'; // Case 1: No Dependency Array (DANGEROUS: Runs after EVERY single render!) useEffect(() => { console.log('Runs after initial mount AND every state/prop update!'); }); // Case 2: Empty Dependency Array [] (Runs ONCE on initial Mount only) useEffect(() => { console.log('Runs exactly ONCE when the component first appears on screen.'); }, []); // Case 3: With Dependencies [userId] (Runs on Mount + whenever userId changes) useEffect(() => { console.log(`Fetching new data because userId changed to: ${userId}`); }, [userId]);
03. The Cleanup Function
If your effect creates an ongoing background process (like a recurring setInterval or a window addEventListener), it will continue running in memory even after the user navigates away from that page, causing severe performance memory leaks!
To prevent this, return a cleanup function from your effect:
import { useState, useEffect } from 'react'; export default function WindowWidthTracker() { const [windowWidth, setWindowWidth] = useState(window.innerWidth); useEffect(() => { function handleResize() { setWindowWidth(window.innerWidth); } // 1. Set up the event listener on Mount window.addEventListener('resize', handleResize); console.log('Resize listener attached.'); // 2. Return cleanup function (Runs on Unmount or before re-running effect) return () => { window.removeEventListener('resize', handleResize); console.log('Listener cleaned up! No memory leaks.'); }; }, []); // Empty array ensures listener is only attached once return <p>Current Screen Width: <strong>{windowWidth}px</strong></p>; }
When does the cleanup function returned from a useEffect execute?
The cleanup function runs when the component is unmounted from the DOM and before the effect runs again due to dependency updates, cleaning up lingering subscriptions or timers.
Objective: Practice timers and cleanup in useEffect.
- Create a component
DigitalClock.jsx. - Use
useStateto store the current time string (new Date().toLocaleTimeString()). - Use
useEffectwithsetIntervalto update the time state every 1,000 milliseconds. - Return a cleanup function with
clearInterval(). - Also update
document.title = `Clock: ${time}`inside an effect so the browser tab displays the live time!
Data Fetching & REST APIs
Connect your React frontend to real-world cloud APIs. Learn how to fetch data asynchronously inside useEffect, manage the 3 fundamental states of every network request (Loading, Error, Data), and cancel in-flight requests with AbortController.
- How to perform asynchronous HTTP requests with
fetchandasync/await - The 3 UI States:
isLoading,error, anddata - Preventing race conditions and unmounted state updates with
AbortController - Triggering refetches with dependency parameters (e.g. pagination, search queries)
- Handling HTTP error codes (404, 500) that
fetch()does not reject automatically
01. The Standard Data Fetching Pattern
Because the callback function passed to useEffect cannot be declared as async directly (it must return either nothing or a cleanup function), we declare an async function inside the effect and call it immediately:
import { useState, useEffect } from 'react'; export default function UserList() { const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { // AbortController allows canceling request if component unmounts mid-flight const controller = new AbortController(); async function fetchUsers() { try { setIsLoading(true); setError(null); const response = await fetch('https://jsonplaceholder.typicode.com/users', { signal: controller.signal }); if (!response.ok) { throw new Error(`HTTP Error: ${response.status} ${response.statusText}`); } const data = await response.json(); setUsers(data); } catch (err) { if (err.name !== 'AbortError') { setError(err.message); } } finally { setIsLoading(false); } } fetchUsers(); return () => controller.abort(); // Cleanup: abort network request on unmount }, []); // 1. Loading State if (isLoading) return <div className="loading-spinner">Fetching developers...</div>; // 2. Error State if (error) return <div className="error-alert">Error loading data: {error}</div>; // 3. Success State return ( <div className="users-container"> <h2>Active Community Members ({users.length})</h2> <div className="user-grid"> {users.map(user => ( <div key={user.id} className="user-card"> <h3>{user.name}</h3> <p>📧 {user.email}</p> <p>🏢 {user.company.name}</p> </div> ))} </div> </div> ); }
02. Triggering API Calls on User Search / Page Change
To refetch data whenever a query parameter or page number changes, simply add that variable to the dependency array:
const [postId, setPostId] = useState(1); const [post, setPost] = useState(null); useEffect(() => { fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`) .then(res => res.json()) .then(data => setPost(data)); }, [postId]); // Automatically re-runs whenever postId updates!
Why can you NOT write useEffect(async () => { ... }, []) directly?
An async function implicitly returns a Promise. React expects the effect callback to return a cleanup function or undefined, which is why you declare the async function inside the effect body.
Objective: Fetch dynamic data from the public GitHub API.
- Create an input where a user types any GitHub username (default:
"facebook"). - Include a "Search" button that triggers a fetch to
https://api.github.com/users/{username}. - Display the user's avatar image, name, bio, public repo count, and follower count.
- Properly display an error message if the user does not exist (HTTP 404).
DOM Refs with useRef & Mutable Values
Learn how to access real DOM elements and manage mutable values that persist across renders without triggering a re-render. Understand the difference between useState and useRef, auto-focusing inputs, and tracking intervals.
- What
useRefis and the{ current: initialValue }reference container - Directly accessing and manipulating DOM nodes (focus, scroll, measurements)
- Storing mutable values (timer IDs, click counts) without triggering expensive re-renders
- Comparing
useStatevsuseRefvs plain variables - When to use refs vs when to stick to standard declarative React state
01. Accessing the DOM: Auto-focusing an Input
While React encourages you to declare your UI declaratively, sometimes you need imperative access to a raw DOM element (e.g. focusing an input field, triggering audio/video playback, or measuring canvas dimensions):
import { useRef, useEffect } from 'react'; export default function AutoSearch() { // 1. Create a ref container const searchInputRef = useRef(null); useEffect(() => { // 2. Once mounted, searchInputRef.current points directly to the real <input> DOM node! searchInputRef.current.focus(); }, []); return ( <div> <input ref={searchInputRef} type="search" placeholder="Cursor starts focused here automatically..." /> </div> ); }
02. Storing Mutable Values Without Re-rendering
Here is the second superpower of useRef: it acts as an instance variable container. If you update ref.current = newValue, the value is preserved across all renders, but it does NOT trigger a re-render.
This is the perfect pattern for storing timer IDs, audio controllers, or tracking render counts:
import { useState, useRef } from 'react'; export default function Stopwatch() { const [seconds, setSeconds] = useState(0); const [isRunning, setIsRunning] = useState(false); // Store timer interval ID in a ref so mutating it doesn't cause extra re-renders const timerIdRef = useRef(null); function handleStart() { if (isRunning) return; setIsRunning(true); timerIdRef.current = setInterval(() => { setSeconds(prev => prev + 1); }, 1000); } function handleStop() { clearInterval(timerIdRef.current); timerIdRef.current = null; setIsRunning(false); } function handleReset() { handleStop(); setSeconds(0); } return ( <div className="stopwatch-box"> <h1>{seconds}s</h1> <button onClick={handleStart} disabled={isRunning}>Start</button> <button onClick={handleStop} disabled={!isRunning}>Pause</button> <button onClick={handleReset}>Reset</button> </div> ); }
What happens when you update a ref via myRef.current = 42?
Updating useRef.current is purely synchronous and does NOT trigger a re-render. It is intended for values that do not directly affect the visual output of the component.
Objective: Track how many times a component renders without causing an infinite loop.
- Create a component with an input text box controlled by
useState. - Create a ref
const renderCount = useRef(0). - In a
useEffectwith no dependency array (runs on every render), incrementrenderCount.current += 1. - Display: "This component has re-rendered: X times" as you type in the input.
Building Custom React Hooks
Elevate your code quality from junior to senior by extracting and sharing stateful logic. Learn the official Rules of Hooks and create custom hooks for localStorage synchronization, API fetching, and debounced search.
- Why custom hooks are the ultimate tool for DRY (Don't Repeat Yourself) logic
- The 2 golden Rules of React Hooks
- Building
useLocalStorageto persist state in the browser automatically - Building
useFetchfor clean one-line data fetching with loading/error flags - Building
useDebounceto throttle rapid search keystrokes
01. What is a Custom Hook?
A custom hook is simply a regular JavaScript function whose name starts with "use" (like useLocalStorage, useAuth, or useWindowSize) and that calls other React hooks (like useState, useEffect, useRef) inside itself.
- Only call Hooks at the top level: Never call hooks inside loops, conditional
ifstatements, or nested functions. React relies on the exact execution order of hooks across renders. - Only call Hooks from React functions: Only call hooks from React functional components or other custom hooks. Never call them from ordinary helper scripts.
02. Building useLocalStorage
Have you ever wanted state to automatically persist in localStorage so it survives page reloads? Let's build a custom hook that behaves just like useState, but syncs to browser storage:
import { useState, useEffect } from 'react'; export function useLocalStorage(key, fallbackValue) { // Initialize state from localStorage or fallback const [value, setValue] = useState(() => { try { const saved = localStorage.getItem(key); return saved !== null ? JSON.parse(saved) : fallbackValue; } catch (e) { console.error('Failed to read localStorage:', e); return fallbackValue; } }); // Automatically sync changes back into localStorage useEffect(() => { try { localStorage.setItem(key, JSON.stringify(value)); } catch (e) { console.error('Failed to write localStorage:', e); } }, [key, value]); return [value, setValue]; }
Now, any component in your entire project can persist state with a single line of code:
import { useLocalStorage } from './hooks/useLocalStorage'; export default function ThemeToggle() { // Persists user preference across browser restarts automatically! const [theme, setTheme] = useLocalStorage('user-theme', 'dark'); return ( <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}> Current Theme: {theme} (Click to toggle) </button> ); }
03. Building useFetch
import { useState, useEffect } from 'react'; export function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let isMounted = true; setLoading(true); fetch(url) .then(res => { if (!res.ok) throw new Error(`HTTP Error ${res.status}`); return res.json(); }) .then(result => { if (isMounted) setData(result); }) .catch(err => { if (isMounted) setError(err.message); }) .finally(() => { if (isMounted) setLoading(false); }); return () => { isMounted = false; }; }, [url]); return { data, loading, error }; }
What naming convention is required for React to recognize a function as a custom hook and enforce hook rules?
React's ESLint plugin and internals rely on the use prefix convention (e.g., useCounter) to automatically enforce the Rules of Hooks.
useToggle Custom HookObjective: Abstract boolean state toggling into a reusable utility.
- Create
src/hooks/useToggle.jsthat accepts an optionalinitialValue = false. - Return an array:
[value, toggle, setTrue, setFalse]. - Use this hook in an FAQ Accordion component to expand/collapse questions cleanly.
🛠️ Project 2: Live Weather & Crypto Dashboard
Build an interactive Live Dashboard application consuming real REST APIs, utilizing your custom hooks, managing search queries, handling loading/error indicators, and storing favorite tokens in localStorage.
- Live API Data: Fetch real-time cryptocurrency prices from CoinGecko public API
- Custom Hook Integration: Use
useLocalStorageto remember pinned favorites - Search & Filter: Real-time filter by coin name or symbol
- Network States: Polished animated skeletons and error recovery
01. Full Annotated Component Code
import { useState, useEffect } from 'react'; import { useLocalStorage } from './hooks/useLocalStorage'; export default function CryptoDashboard() { const [coins, setCoins] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [search, setSearch] = useState(''); // Persist pinned coin IDs in localStorage const [pinned, setPinned] = useLocalStorage('pinned_coins', ['bitcoin', 'ethereum']); async function fetchMarketData() { try { setLoading(true); setError(null); const url = 'https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=12&page=1'; const res = await fetch(url); if (!res.ok) throw new Error('Rate limit or API unavailable'); const data = await res.json(); setCoins(data); } catch (err) { setError(err.message); } finally { setLoading(false); } } useEffect(() => { fetchMarketData(); }, []); function togglePin(coinId) { setPinned(prev => prev.includes(coinId) ? prev.filter(id => id !== coinId) : [...prev, coinId] ); } const filteredCoins = coins.filter(c => c.name.toLowerCase().includes(search.toLowerCase()) || c.symbol.toLowerCase().includes(search.toLowerCase()) ); return ( <div className="dashboard-container"> <div className="dash-header"> <h1>Live Market Ticker</h1> <button onClick={fetchMarketData} className="btn-refresh">↻ Refresh Prices</button> </div> <input type="search" placeholder="Filter by coin or symbol (BTC, Solana, ETH)..." value={search} onChange={e => setSearch(e.target.value)} className="dash-search" /> {loading && <div className="loading-card">Updating live market feeds...</div>} {error && <div className="error-card">{error} <button onClick={fetchMarketData}>Retry</button></div>} {!loading && !error && ( <div className="crypto-grid"> {filteredCoins.map(coin => { const isPinned = pinned.includes(coin.id); const isPositive = coin.price_change_percentage_24h >= 0; return ( <div key={coin.id} className={`crypto-card ${isPinned ? 'pinned' : ''}`}> <div className="coin-top"> <img src={coin.image} alt={coin.name} width="32" height="32" /> <div> <h3>{coin.name}</h3> <span className="symbol">{coin.symbol.toUpperCase()}</span> </div> <button onClick={() => togglePin(coin.id)} className="pin-btn"> {isPinned ? '★' : '☆'} </button> </div> <div className="price-row"> <span className="price">${coin.current_price.toLocaleString()}</span> <span className={`change ${isPositive ? 'up' : 'down'}`}> {isPositive ? '+' : ''}{coin.price_change_percentage_24h?.toFixed(2)}% </span> </div> </div> ); })} </div> )} </div> ); }
- Add a currency switcher to view prices in EUR (€) or GBP (£) instead of USD ($).
- Add an auto-refresh timer that automatically triggers
fetchMarketData()every 60 seconds usinguseEffect.
Lifting State & The Context API
Solve the notorious "Prop Drilling" dilemma. Learn how to lift state up to common ancestors, and utilize React's built-in Context API to provide global state (such as user authentication, themes, and shopping carts) to any component in your tree without passing props.
- The Prop Drilling problem and when it hurts maintainability
- Lifting State Up: finding the nearest common parent component
- The 3-step Context workflow:
createContext,Context.Provider, anduseContext - Building a production-ready
AuthContextandThemeContext - When to use Context vs when to stick to simple props
01. The Prop Drilling Problem
Imagine your root App component has a logged-in user object. A deep child component (e.g. UserAvatar nested inside Navbar -> NavMenu -> UserProfileSection -> UserAvatar) needs access to this user.
Without Context, every intermediate component must accept the user prop and forward it down, even if that intermediate component has zero use for it. This tedious, brittle pattern is known as Prop Drilling.
02. The Context API Solution
React's Context API allows you to "teleport" data directly to any component in the tree, regardless of depth:
import { createContext, useContext, useState } from 'react'; // Step 1: Create the Context object const ThemeContext = createContext(); // Step 2: Create a Custom Provider Component export function ThemeProvider({ children }) { const [theme, setTheme] = useState('dark'); function toggleTheme() { setTheme(prev => prev === 'dark' ? 'light' : 'dark'); } return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> <div className={`theme-wrapper ${theme}`}> {children} </div> </ThemeContext.Provider> ); } // Step 3: Create a clean custom hook for consumption export function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error('useTheme must be used within a ThemeProvider'); } return context; }
03. Consuming Context Anywhere
Wrap your top-level app inside <ThemeProvider> in main.jsx or App.jsx:
// 1. Wrap the app tree in main.jsx <ThemeProvider> <App /> </ThemeProvider> // 2. Consume directly in ANY nested component without props! import { useTheme } from './context/ThemeContext'; export default function DarkModeButton() { const { theme, toggleTheme } = useTheme(); return ( <button onClick={toggleTheme}> Current Mode: {theme === 'dark' ? '🌙 Dark' : '☀️ Light'} </button> ); }
What problem does the React Context API primarily solve?
Context allows data (like user auth or themes) to be accessed by any component in the sub-tree without manual prop drilling through intermediary components.
Objective: Create an authentication provider and custom hook.
- Create
AuthContext.jsxwith states:user(null or{ name: 'Alice', email: '[email protected]' }) and functions:login(userData)andlogout(). - Wrap your app in
<AuthProvider>. - Build a
Navbarthat readsuseAuth(): if logged in, display "Welcome, Alice" and a Log Out button. If null, display a Login button.
Routing with React Router v6+
Transform your React application into a multi-page Single Page App. Master client-side routing with React Router v6+, dynamic URL parameters with useParams, programmatic redirects with useNavigate, and 404 catch-all pages.
- Client-side routing vs traditional server multi-page architecture
- Installing and setting up
react-router-dom - Core components:
BrowserRouter,Routes,Route,Link, andNavLink - Dynamic routes with URL parameters:
/products/:productIdand theuseParamshook - Programmatic navigation with the
useNavigatehook - Catch-all 404 Not Found route handling (
path="*")
01. Installation & Basic Setup
In standard HTML websites, clicking a link causes the browser to download an entirely new HTML page from the server, causing a blank flash. React Router intercepts link clicks, updates the browser URL bar via the HTML5 History API, and swaps components instantly without refreshing the page.
npm install react-router-dom
02. Defining Routes in Your App
import { BrowserRouter, Routes, Route, Link, NavLink } from 'react-router-dom'; import Home from './pages/Home'; import About from './pages/About'; import ProductDetail from './pages/ProductDetail'; import NotFound from './pages/NotFound'; export default function App() { return ( <BrowserRouter> <nav className="main-nav"> {/* NavLink automatically adds an "active" CSS class when matching current URL! */} <NavLink to="/" end>Home</NavLink> <NavLink to="/about">About</NavLink> <NavLink to="/contact">Contact</NavLink> </nav> <main className="page-container"> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> {/* Dynamic route parameter :id */} <Route path="/products/:id" element={<ProductDetail />} /> {/* 404 Catch-All Route */} <Route path="*" element={<NotFound />} /> </Routes> </main> </BrowserRouter> ); }
03. Reading Dynamic URL Parameters with useParams
When a user navigates to /products/item-849, how does the component know which item ID was requested? The useParams hook extracts URL tokens dynamically:
import { useParams, useNavigate } from 'react-router-dom'; export default function ProductDetail() { const { id } = useParams(); const navigate = useNavigate(); return ( <div className="detail-card"> <h1>Product ID: #{id}</h1> <p>Displaying live specifications for product #{id}...</p> {/* Programmatic navigation back to home */} <button onClick={() => navigate('/')}> ← Back to Catalog </button> </div> ); }
Why should you use <Link to="/about"> instead of <a href="/about"> in a React SPA?
<Link> prevents the browser from refreshing the entire page, preserving in-memory JavaScript state and delivering an instant Single Page Application experience.
Objective: Create a blog with home, post details, and 404 pages.
- Set up routes for
/(lists 3 blog posts),/posts/:slug(post details), and*(404 page). - Clicking a blog post card on Home links to its respective slug.
- The PostDetail page reads
useParams()and displays the corresponding title and text. - If the user enters an invalid URL, show a styled 404 page with a "Return Home" button.
Performance Optimization: useMemo, useCallback & React.memo
Master optimizing React applications for high speed and scale. Understand when React re-renders, cache heavy calculations with useMemo, preserve function references with useCallback, prevent child re-renders with React.memo, and split code with React.lazy.
- The exact conditions that trigger component re-renders
- Caching expensive computations using the
useMemohook - Preserving stable function references across renders using
useCallback - Skipping child re-renders with
React.memoshallow comparison - Code splitting with
React.lazyandSuspenseto shrink bundle sizes
01. Why and When Does React Re-render?
A React component re-renders under two primary circumstances:
- Its internal state changes: Calling a state setter schedules a re-render.
- Its parent component re-renders: By default, whenever a parent component re-renders, ALL of its children re-render as well, even if their props have not changed!
02. Caching Calculations with useMemo
If you have a function that performs heavy mathematical calculations or sorts a list of 10,000 items, you don't want it recalculating on unrelated state changes (like typing into a search input):
import { useState, useMemo } from 'react'; function heavyCalculation(num) { console.log('Executing expensive calculation...'); let total = 0; for (let i = 0; i < 50000000; i++) { total += num; } return total; } export default function ComputeBox() { const [number, setNumber] = useState(10); const [dark, setDark] = useState(false); // useMemo only recalculates when "number" changes, NOT when "dark" toggles! const calculationResult = useMemo(() => { return heavyCalculation(number); }, [number]); return ( <div className={dark ? 'dark' : 'light'}> <h3>Result: {calculationResult}</h3> <button onClick={() => setNumber(n => n + 1)}>Change Number</button> <button onClick={() => setDark(d => !d)}>Toggle Theme (Fast!)</button> </div> ); }
03. Preserving Callbacks with useCallback
Every time a component re-renders, any function defined inside it is created as a brand-new function in memory. Passing this new function down as a prop breaks React.memo child optimizations:
import React, { useState, useCallback } from 'react'; // Child only re-renders if onItemDelete changes! const TaskItem = React.memo(({ task, onItemDelete }) => { console.log('Rendered TaskItem:', task.text); return <li onClick={() => onItemDelete(task.id)}>{task.text}</li>; }); export default function TaskList() { const [tasks, setTasks] = useState([...]); // useCallback keeps the exact same function reference between renders const handleDelete = useCallback((id) => { setTasks(prev => prev.filter(t => t.id !== id)); }, []); return ( <ul> {tasks.map(t => <TaskItem key={t.id} task={t} onItemDelete={handleDelete} />)} </ul> ); }
04. Code Splitting with React.lazy & Suspense
Why make your users download a 5MB bundle when they only visit the Home page? Lazy-load heavier routes on demand:
import React, { Suspense, lazy } from 'react'; // Components are loaded only when the user actually navigates to them const AnalyticsDashboard = lazy(() => import('./pages/Analytics')); function App() { return ( <Suspense fallback={<div className="page-skeleton">Loading page...</div>}> <AnalyticsDashboard /> </Suspense> ); }
What is the difference between useMemo and useCallback?
useMemo(() => fn(), [deps]) caches the returned value of a calculation. useCallback(fn, [deps]) caches the function instance itself across renders.
Objective: Profile and memoize a searchable dataset.
- Generate a mock array of 2,000 items with random prices and categories.
- Wrap the filtering logic in
useMemotied to the search query. - Add a separate unrelated counter button. Verify in the console that filtering does NOT run when clicking the counter!
🛠️ Capstone Project: Full-Featured E-Commerce Store
Congratulations on reaching the final capstone! In this comprehensive project, you will combine all 20 lectures—Components, Props, useState, Events, Conditional Rendering, Lists & Keys, Forms, useEffect, Custom Hooks, Context API, and Routing—into a production-grade E-Commerce Store.
- Product Catalog: Dynamic grid with live category filters, search input, and sorting
- CartContext & Global State: Add to cart, quantity adjustments, item removal, and subtotal calculation
- Persistent Storage: Cart state automatically persisted using custom
useLocalStorage - Multi-Page Routing: Catalog page, Product details, Shopping Cart drawer, and Checkout Modal
- Toast Notifications: Real-time feedback ("Added to cart!")
01. The Global CartContext
Create src/context/CartContext.jsx to manage shopping cart operations globally across all pages:
import { createContext, useContext, useState, useEffect } from 'react'; const CartContext = createContext(); export function CartProvider({ children }) { const [cart, setCart] = useState(() => { try { const saved = localStorage.getItem('cart_items'); return saved ? JSON.parse(saved) : []; } catch (e) { return []; } }); useEffect(() => { localStorage.setItem('cart_items', JSON.stringify(cart)); }, [cart]); function addToCart(product) { setCart(prev => { const existing = prev.find(item => item.id === product.id); if (existing) { return prev.map(item => item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item ); } return [...prev, { ...product, quantity: 1 }]; }); } function removeFromCart(productId) { setCart(prev => prev.filter(item => item.id !== productId)); } function updateQuantity(productId, delta) { setCart(prev => prev.map(item => { if (item.id === productId) { const newQty = item.quantity + delta; return newQty > 0 ? { ...item, quantity: newQty } : item; } return item; })); } function clearCart() { setCart([]); } const cartCount = cart.reduce((sum, item) => sum + item.quantity, 0); const cartTotal = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0); return ( <CartContext.Provider value={{ cart, addToCart, removeFromCart, updateQuantity, clearCart, cartCount, cartTotal }}> {children} </CartContext.Provider> ); } export function useCart() { return useContext(CartContext); }
02. Product Catalog & Filter Page
import { useState } from 'react'; import { useCart } from '../context/CartContext'; const PRODUCTS = [ { id: 'p-1', name: 'Wireless Noise-Canceling Headphones', price: 149.99, category: 'Audio', rating: 4.8 }, { id: 'p-2', name: 'Mechanical RGB Keyboard (Hot-swap)', price: 99.50, category: 'Peripherals', rating: 4.9 }, { id: 'p-3', name: 'Ultra-Wide 34-inch 144Hz Monitor', price: 449.00, category: 'Displays', rating: 4.7 }, { id: 'p-4', name: 'Ergonomic Aluminum Laptop Stand', price: 39.99, category: 'Accessories', rating: 4.6 }, { id: 'p-5', name: 'Studio Condenser USB Microphone', price: 89.00, category: 'Audio', rating: 4.8 } ]; export default function Catalog() { const { addToCart } = useCart(); const [category, setCategory] = useState('All'); const [search, setSearch] = useState(''); const [toast, setToast] = useState(''); function handleAdd(product) { addToCart(product); setToast(`Added "${product.name}" to cart!`); setTimeout(() => setToast(''), 3000); } const filtered = PRODUCTS.filter(p => { const matchCat = category === 'All' || p.category === category; const matchSearch = p.name.toLowerCase().includes(search.toLowerCase()); return matchCat && matchSearch; }); return ( <div className="catalog-view"> {toast && <div className="toast-notification">✓ {toast}</div>} <div className="filter-bar"> <input type="search" placeholder="Search gear..." value={search} onChange={e => setSearch(e.target.value)} /> <div className="pill-group"> {['All', 'Audio', 'Peripherals', 'Displays', 'Accessories'].map(cat => ( <button key={cat} className={category === cat ? 'pill active' : 'pill'} onClick={() => setCategory(cat)} > {cat} </button> ))} </div> </div> <div className="product-grid"> {filtered.map(product => ( <div key={product.id} className="product-card"> <div className="tag">{product.category}</div> <h3>{product.name}</h3> <div className="rating">★ {product.rating} / 5.0</div> <div className="card-footer"> <span className="price">${product.price.toFixed(2)}</span> <button onClick={() => handleAdd(product)} className="btn-add"> + Add to Cart </button> </div> </div> ))} </div> </div> ); }
03. Shopping Cart Drawer & Checkout
import { useCart } from '../context/CartContext'; export default function CartDrawer({ isOpen, onClose }) { const { cart, removeFromCart, updateQuantity, cartTotal, clearCart } = useCart(); if (!isOpen) return null; return ( <div className="drawer-overlay" onClick={onClose}> <div className="drawer-content" onClick={e => e.stopPropagation()}> <div className="drawer-head"> <h2>Your Shopping Cart</h2> <button onClick={onClose} className="close-btn">×</button> </div> <div className="cart-items-scroll"> {cart.length === 0 ? ( <div className="cart-empty">Your cart is currently empty.</div> ) : ( cart.map(item => ( <div key={item.id} className="cart-item-row"> <div className="item-info"> <h4>{item.name}</h4> <span>${item.price.toFixed(2)} each</span> </div> <div className="qty-controls"> <button onClick={() => updateQuantity(item.id, -1)}>-</button> <span>{item.quantity}</span> <button onClick={() => updateQuantity(item.id, 1)}>+</button> </div> <button onClick={() => removeFromCart(item.id)} className="remove-btn"> 🗑 </button> </div> )) )} </div> {cart.length > 0 && ( <div className="drawer-foot"> <div className="total-row"> <span>Subtotal:</span> <strong>${cartTotal.toFixed(2)}</strong> </div> <button onClick={() => { alert('Order placed successfully! Thank you.'); clearCart(); onClose(); }} className="checkout-btn" > Proceed to Checkout (${cartTotal.toFixed(2)}) </button> </div> )} </div> </div> ); }
04. Your React Developer Certification Path
Congratulations on completing the React Mastery Course on Code Tutorium! You now possess comprehensive, practical mastery of:
- Declarative Component-driven architecture and Virtual DOM diffing
- Modern tooling with Vite, ES Modules, and production bundling
- JSX formatting, fragments, and expression evaluation
- Unidirectional data flow, destructuring props, and children slots
- Interactive state management with
useStateand immutable state recipes - Controlled forms, multi-field state objects, and form validation
- Component lifecycles, REST API consumption, and cleanup with
useEffect - Direct DOM refs and mutable instance variables with
useRef - Extracting custom hooks for DRY, reusable application architecture
- Global state management without third-party boilerplate using the
Context API - Multi-page Single Page App navigation using
React Router v6+ - Optimized performance using
useMemo,useCallback,React.memo, and code splitting
Deploy your capstone project live to Vercel or Netlify, link your GitHub repository, and add it to your portfolio resume. You are now prepared to build production web applications or explore full-stack frameworks like Next.js!