Skip to content

React

React is a popular JavaScript library for building dynamic, interactive user interfaces. It excels at creating Single Page Applications (SPAs) where content updates efficiently without requiring full page reloads.

Core Concepts:

  • Declarative: You describe what the UI should look like for a given state, and React handles updating the actual browser DOM efficiently.
  • Component-Based: UIs are built by composing small, reusable pieces called components. This makes managing and scaling complex interfaces easier.
  • Reusable UI Components: Components encapsulate markup, logic, and styling, promoting reusability across your application.

Building SPAs: Vanilla JS vs. React

Both plain JavaScript and React can build Single Page Applications.

  • Vanilla JavaScript: Requires more manual effort for tasks like managing UI updates (DOM manipulation), handling application state, and implementing routing.
  • React: Provides abstractions (like the Virtual DOM and state management hooks) that simplify these tasks, especially for complex applications.

Why Was React Created? The “Phantom Message” Problem

Facebook initially faced UI synchronization challenges. A common example was the “Phantom Message Problem” (or Ghost Message):

  1. A user receives new messages.
  2. A notification badge appears on the chat icon.
  3. The user opens the chat and reads the messages.
  4. The notification badge sometimes wouldn’t disappear immediately or update correctly across different parts of the UI.

This difficulty in keeping the UI consistently synchronized with the underlying application state was a major motivation for developing React’s declarative and reactive approach to UI updates.


Example: Simple Counter (Vanilla JS vs. React)

Let’s compare building a simple counter.

Vanilla JavaScript: Manually selecting DOM elements and updating their content.

index.html (Vanilla JS)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vanilla JS Counter</title>
<style> /* Basic Styles */
body { font-family: sans-serif; text-align: center; margin-top: 50px; }
button { padding: 10px 15px; margin: 5px; cursor: pointer; }
h2 { margin-top: 20px; }
</style>
</head>
<body>
<h1>Vanilla JS Counter</h1>
<div id="buttonContainer"></div>
</body>
<script>
let counter = 0;
const buttonContainer = document.getElementById("buttonContainer");
const button1 = document.createElement("button");
const button2 = document.createElement("button");
const counterDisplay = document.createElement("h2");
// Function to manually update the DOM
function updateUI() {
button1.textContent = `Add Value : ${counter}`;
button2.textContent = `Remove Value : ${counter}`;
counterDisplay.textContent = `Counter : ${counter}`;
}
button1.addEventListener("click", () => {
if(counter < 20) {
counter++;
updateUI(); // Manually call update
}
});
button2.addEventListener("click", () => {
if(counter > 0) {
counter--;
updateUI(); // Manually call update
}
});
// Initial UI setup
updateUI();
buttonContainer.appendChild(button1);
buttonContainer.appendChild(document.createElement("br"));
buttonContainer.appendChild(button2);
buttonContainer.appendChild(document.createElement("br"));
buttonContainer.appendChild(counterDisplay);
</script>
</html>

React: Declaring the UI based on state. React handles the DOM updates automatically when the state changes.

  • Changes are managed using hooks.
  • useState is the fundamental hook for adding state to functional components.
App.jsx (React)
import React, { useState } from "react";
function App() {
// Declare state variable 'counter' and updater function 'setCounter'
const [counter, setCounter] = useState(0);
const increment = () => {
if (counter < 20) {
// Tell React to update the state
setCounter(counter + 1);
// React will re-render the component with the new 'counter' value
}
};
const decrement = () => {
if (counter > 0) {
// Tell React to update the state
setCounter(counter - 1);
// React will re-render
}
};
// Inline styles (example purpose, often done via CSS/libraries)
const containerStyle = { /* ...styles */ };
const buttonStyle = { /* ...styles */ };
const decrementButtonStyle = { ...buttonStyle, background: "#DC3545" };
const counterStyle = { /* ...styles */ };
// Return the JSX describing the UI based on current 'counter' state
return (
<div style={containerStyle}>
<button style={buttonStyle} onClick={increment}>
Add Value : {counter}
</button>
<button style={decrementButtonStyle} onClick={decrement}>
Remove Value : {counter}
</button>
<h2 style={counterStyle}>Counter: {counter}</h2>
</div>
);
}
// (Assume styles are defined as in your original example)
export default App;

Install React (using Vite)

Vite is a modern, fast build tool recommended for starting React projects.

Terminal window
npm create vite@latest your-project-name --template react-ts
# Follow prompts: select React -> TypeScript (or JavaScript)
cd your-project-name
npm install
npm run dev

Common React-Related Libraries:

  • react: The core React library containing fundamental features like components and hooks.
  • react-dom: The bridge between React and the browser DOM. It handles rendering components into the actual webpage.
  • react-router-dom: The standard library for handling routing in React web applications.
  • Build Tool Scripts (e.g., vite, formerly react-scripts in Create React App): Handle bundling, development server, and production builds.
  • react-native: A separate framework (not just a library) built by Meta for creating native mobile apps (iOS/Android) using React principles and JavaScript.

Behind the Scenes: How React Works

Virtual DOM

React uses a Virtual DOM to optimize performance.

  • It’s a lightweight JavaScript representation (an object tree) of the actual browser DOM.
  • When state changes, React first updates the Virtual DOM.

Reconciliation (Diffing Algorithm)

After updating the Virtual DOM, React compares the new Virtual DOM tree with the previous one. This comparison process is called Reconciliation.

  • React uses a diffing algorithm to efficiently identify the specific changes between the two virtual trees.
  • It then calculates the minimum number of operations needed to update the actual browser DOM to match the new Virtual DOM.
  • Only the changed parts of the real DOM are updated, which is much faster than re-rendering everything.
Virtual DOM (New) Actual DOM (Old)
───────────────── ─────────────────
<div> <div>
│ │
┌────┴────────┐ ┌─────┴────┐
│ │ │ │ │ │ │
<img> <h2> <p> <btn> ═══> <img> <h2> <p> (Button is missing)
│ │ │ │ Diffing │ │ │
props text text text Algorithm props text text
React Diff identifies:
- Button needs to be added.
- Other elements/props might have changed.
=> Only the necessary changes are applied to the Actual DOM.

Batching

If multiple state updates happen close together (e.g., within the same event handler), React often batches them. It performs all the state updates first and then triggers a single re-render at the end, further optimizing performance.


Understanding the Output: From Component to Tree

A React component function returns a description of the UI, often written using JSX.

Card.jsx
const Card = () => {
return (
<div>
<img src="img.jpg" alt="img" />
<h2>Card Title</h2>
<p>This is a card</p>
</div>
);
};
export default Card;

React internally represents this as a tree of objects (part of the Virtual DOM):

Internal Representation (Simplified)
// React generates a tree structure like this:
const elementTree = {
type: "div",
props: {
children: [
{ type: "img", props: { src: "img.jpg", alt: "img" } },
{ type: "h2", props: { children: "Card Title" } },
{ type: "p", props: { children: "This is a card" } }
]
}
};

This object tree is platform-agnostic. react-dom knows how to turn this tree into browser elements, react-native into native elements, and you could even write a custom renderer.

Example: Basic custom renderer logic (illustrative)

Custom Renderer Concept
function render(element, container) {
const domElement = document.createElement(element.type);
// Handle props (simplified)
Object.keys(element.props).forEach(propName => {
if (propName !== 'children') {
domElement.setAttribute(propName, element.props[propName]);
}
});
// Handle children
const children = element.props.children || [];
children.forEach(child => {
if (typeof child === 'string') {
domElement.appendChild(document.createTextNode(child));
} else {
render(child, domElement); // Recursively render child elements
}
});
container.appendChild(domElement);
}
// Assume elementTree from above and container is <div id="root">
// render(elementTree, document.getElementById('root'));

What is JSX?

JSX (JavaScript XML) is a syntax extension for JavaScript that looks very similar to HTML. It allows you to write UI structures declaratively within your JavaScript code.

const element = <h1>Hello, world!</h1>;

How JSX is Converted: Build tools (like Vite or Create React App) use a compiler called Babel to transform JSX syntax into regular JavaScript function calls, specifically React.createElement().

JSX
const element = <h1 className="greeting">Hello, world!</h1>;
Equivalent JavaScript (after Babel)
const element = React.createElement(
"h1",
{ className: "greeting" }, // Props object
"Hello, world!" // Children...
);

JSX: Expressions vs. Statements

You can embed JavaScript expressions within JSX using curly braces {}.

const name = "Alice";
const element = <h1>Hello, {name.toUpperCase()}!</h1>; // Evaluates name.toUpperCase()

React’s Snapshot Behavior and State Updates

When a state update function (like setCounter) is called, React doesn’t immediately change the state value within the currently running function. It schedules an update. The component will re-render later with the new state value.

Consider this:

Counter.jsx - Potential Pitfall
import { useState } from "react";
function Counter() {
const [counter, setCounter] = useState(0);
function incrementMultipleTimes() {
// All these calls use the 'counter' value from *this* render (which is 0 initially)
setCounter(counter + 1); // Schedules update to 0 + 1 = 1
setCounter(counter + 1); // Schedules update to 0 + 1 = 1
setCounter(counter + 1); // Schedules update to 0 + 1 = 1
}
// After this function finishes, React batches updates and re-renders.
// The final state will be 1, not 3.
return (/* ... JSX ... */);
}

React Hooks

Hooks are functions that let you “hook into” React state and lifecycle features from function components. They make it possible to use state, effects, context, and more without writing class components.

Basic Hooks

useState

The most fundamental hook. Adds state to a functional component.

  • Syntax: const [state, setState] = useState(initialValue);
  • Returns: An array with two elements: the current state value and a function to update it.
function Counter() {
const [count, setCount] = useState(0); // Initial state is 0
return (
<div>
<p>Count: {count}</p>
{/* Call setCount to schedule a state update and re-render */}
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(prevCount => prevCount - 1)}>Decrement</button>
</div>
);
}

useEffect

Lets you perform side effects in function components. Side effects include data fetching, setting up subscriptions, or manually changing the DOM.

  • Syntax: useEffect(() => { /* Effect code */ return () => { /* Optional cleanup */ }; }, [dependencies]);
  • Dependencies Array: Controls when the effect runs and cleans up.
    • [] (Empty Array): Effect runs only once after the initial render. Cleanup runs only once when the component unmounts.
    • [dep1, dep2] (With Dependencies):
      1. Cleanup function from the previous render runs (if the effect ran before).
      2. The effect function runs if any dependency value has changed since the last render.
    • No Array: Effect runs after every render. Cleanup runs before every subsequent effect run. (Use sparingly).
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
console.log(`Fetching data for user ${userId}`);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
// Optional cleanup (e.g., aborting fetch on unmount/userId change)
// return () => { console.log(`Cleaning up effect for user ${userId}`); };
}, [userId]); // Re-run the effect ONLY if userId changes
if (loading) return <p>Loading...</p>;
return <div>{user ? user.name : 'User not found'}</div>;
}

useContext

Allows components to subscribe to React context without introducing nesting (“prop drilling”). Accesses shared data like themes, user auth, etc.

  • Syntax: const value = useContext(MyContext);
// 1. Create Context
const ThemeContext = createContext('light'); // Default value
// 2. Provide Context (usually higher up the tree)
function App() {
return (
<ThemeContext.Provider value="dark">
<ThemedButton />
</ThemeContext.Provider>
);
}
// 3. Consume Context
function ThemedButton() {
const theme = useContext(ThemeContext); // Accesses 'dark'
return <button className={`theme-${theme}`}>I'm a {theme} button</button>;
}```
#### `useRef`
Returns a mutable ref object whose `.current` property is initialized to the passed argument. The ref object persists for the full lifetime of the component.
* **Syntax:** `const myRef = useRef(initialValue);`
* **Use Cases:**
* Accessing DOM nodes directly (e.g., focusing an input).
* Storing mutable values that *don't* cause a re-render when changed (unlike state).
```jsx
function TextInputWithFocusButton() {
const inputEl = useRef(null); // Ref to hold the input DOM element
const onButtonClick = () => {
// inputEl.current points to the mounted input element
inputEl.current.focus();
};
return (
<>
{/* Attach the ref to the input element */}
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}

useMemo

Memoizes the result of an expensive calculation. It re-runs the calculation only if one of the dependencies has changed.

  • Syntax: const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
function ExpensiveComponent({ data }) {
// Assume processData is a very slow function
const processedData = useMemo(() => {
console.log("Processing data..."); // Will only log if 'data' changes
return processData(data);
}, [data]); // Only re-calculate if 'data' prop changes
return <div>{/* Render using processedData */}</div>;
}

useCallback

Memoizes a function instance. Useful for passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders.

  • Syntax: const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);
function ParentComponent() {
const [count, setCount] = useState(0);
// Without useCallback, a new handleClick function is created on every render of ParentComponent.
// If ChildComponent is memoized (React.memo), it might re-render unnecessarily.
// const handleClick = () => { console.log('Button clicked'); };
// With useCallback, handleClick function instance is reused as long as dependencies ([]) are same.
const handleClick = useCallback(() => {
console.log('Button clicked');
}, []); // Empty dependency array means function is created once
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment Parent</button>
<ChildComponent onClick={handleClick} />
</div>
);
}
// Assume ChildComponent is wrapped in React.memo
const ChildComponent = React.memo(({ onClick }) => {
console.log("Child rendered");
return <button onClick={onClick}>Click Child</button>;
});

Example: Password Generator (Using Hooks)

Password Generator App
// Import necessary hooks and functions from React.
import React, { useState, useCallback, useEffect, useRef } from 'react';
// Character sets for password generation
const CHAR_SETS = { /* ... as defined before ... */ };
// Main functional component
function App() {
// Core states using useState
const [length, setLength] = useState(8);
const [includeNumbers, setIncludeNumbers] = useState(false);
const [includeSymbols, setIncludeSymbols] = useState(false);
const [password, setPassword] = useState('');
// Ref for DOM access using useRef
const passwordInputRef = useRef(null);
// Memoized password generation function using useCallback
const generatePassword = useCallback(() => {
let chars = CHAR_SETS.lowercase + CHAR_SETS.uppercase;
if (includeNumbers) chars += CHAR_SETS.numbers;
if (includeSymbols) chars += CHAR_SETS.symbols;
const passwordArray = Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]);
setPassword(passwordArray.join(''));
}, [length, includeNumbers, includeSymbols]); // Dependencies
// Memoized copy function using useCallback
const copyPassword = useCallback(() => {
passwordInputRef.current?.select(); // Select text using ref
window.navigator.clipboard.writeText(password);
}, [password]); // Dependency
// Effect to generate password on load or when options change, using useEffect
useEffect(() => {
generatePassword();
}, [generatePassword]); // Dependency (the memoized function itself)
// JSX structure
return (
<div className="w-full max-w-md mx-auto shadow-lg rounded-lg px-4 py-8 my-4 text-orange-500 bg-gray-700">
<h1 className="text-white text-3xl font-bold text-center py-4 mb-4">Password Generator</h1>
<div className="flex shadow rounded-lg overflow-hidden mb-4">
<input
type="text"
value={password}
className="w-full px-4 py-2 text-gray-700 focus:outline-none"
readOnly
ref={passwordInputRef} // Attach ref
/>
<button onClick={generatePassword} className="...">Generate</button>
<button onClick={copyPassword} className="...">Copy</button>
</div>
<div className="flex flex-col space-y-2">
{/* Length Slider */}
<div className="flex items-center space-x-2">
<input type="range" min="1" max="32" value={length} onChange={(e) => setLength(parseInt(e.target.value))} />
<label>Length: {length}</label>
</div>
{/* Checkboxes */}
<div className="flex items-center space-x-2">
<input type="checkbox" checked={includeNumbers} id="num" onChange={() => setIncludeNumbers(prev => !prev)} />
<label htmlFor="num">Include Numbers</label>
</div>
<div className="flex items-center space-x-2">
<input type="checkbox" checked={includeSymbols} id="sym" onChange={() => setIncludeSymbols(prev => !prev)} />
<label htmlFor="sym">Include Symbols</label>
</div>
</div>
</div>
);
}
// Assume CHAR_SETS and CSS classes are defined
export default App;

Custom Hooks

Custom Hooks allow you to extract component logic into reusable functions. They are a core pattern for sharing stateful logic between components without using render props or higher-order components.

  • Naming Convention: Must start with use (e.g., useCounter, useFetch).
  • Purpose: Encapsulate logic that uses one or more standard Hooks (useState, useEffect, etc.).
useCounter.js (Custom Hook)
import { useState } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
// Define actions related to the counter state
const increment = () => setCount(prevCount => prevCount + 1);
const decrement = () => setCount(prevCount => prevCount - 1);
const reset = () => setCount(initialValue);
// Return the state and the actions
return { count, increment, decrement, reset };
}
export default useCounter;
CounterComponent.jsx (Usage)
import useCounter from './useCounter';
function CounterComponent() {
// Use the custom hook like any standard hook
const { count, increment, decrement, reset } = useCounter(5); // Start at 5
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}

Rules of Hooks

These rules are essential for Hooks to work correctly. Linters (like eslint-plugin-react-hooks) help enforce them.

  1. Only Call Hooks at the Top Level: Don’t call Hooks inside loops, conditions (if), or nested functions. Hooks must be called in the same order on every render.
  2. Only Call Hooks from React Functions: Call Hooks from React functional components or other custom Hooks. Don’t call them from regular JavaScript functions.
  3. (Convention) Custom Hooks Must Start with use: This allows linters to enforce the rules correctly for your custom Hooks.

React Router

React Router DOM (react-router-dom) is the standard library for handling navigation and routing in React web applications. It allows you to create different “pages” or views within your SPA and map them to specific URLs.

Installation

Terminal window
npm install react-router-dom

Basic Routing Setup

Wrap your application with BrowserRouter and define routes using Routes and Route.

main.jsx or App.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import App from './App'; // Your main App component (optional)
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import Layout from './components/Layout'; // Example Layout component
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<Routes>
{/* Example with a Layout component */}
<Route path="/" element={<Layout />}>
{/* index route renders at the parent's path "/" */}
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="contact" element={<Contact />} />
{/* Add more routes here */}
</Route>
</Routes>
</BrowserRouter>
</React.StrictMode>
);

Use Link for basic navigation and NavLink when you need to style the active link.

Navbar.jsx
import { Link, NavLink } from 'react-router-dom';
function Navbar() {
return (
<nav>
<Link to="/">Home (Link)</Link>
<NavLink
to="/about"
// Style the link based on whether it's the active route
style={({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? 'red' : 'blue',
})}
>
About (NavLink)
</NavLink>
<NavLink
to="/contact"
className={({ isActive }) => (isActive ? 'active-link-class' : 'inactive-link-class')}
>
Contact (NavLink with Class)
</NavLink>
</nav>
);
}
export default Navbar;

Dynamic Routes (useParams)

Capture segments of the URL as parameters.

Route Setup
// In your Routes definition:
<Route path="/users/:userId" element={<UserProfile />} />
<Route path="/products/:category/:productId" element={<ProductDetail />} />
UserProfile.jsx
import { useParams } from 'react-router-dom';
function UserProfile() {
// useParams returns an object with the matched URL parameters
const { userId } = useParams(); // { userId: 'someValueFromUrl' }
// Fetch user data based on userId...
return <div>Displaying profile for User ID: {userId}</div>;
}

Nested Routes & Layouts (Outlet)

Structure routes hierarchically, often used with shared layout components.

App Routing Setup
// ... imports
import DashboardLayout from './layouts/DashboardLayout';
import Overview from './pages/Dashboard/Overview';
import Settings from './pages/Dashboard/Settings';
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}> {/* Main Layout */}
<Route index element={<Home />} />
<Route path="about" element={<About />} />
{/* Nested Dashboard Routes */}
<Route path="dashboard" element={<DashboardLayout />}>
<Route index element={<Overview />} /> {/* Renders at /dashboard */}
<Route path="settings" element={<Settings />} /> {/* Renders at /dashboard/settings */}
</Route>
<Route path="*" element={<NotFound />} /> {/* 404 Route */}
</Route>
</Routes>
</BrowserRouter>
DashboardLayout.jsx
import { Outlet } from 'react-router-dom';
import DashboardSidebar from './DashboardSidebar';
function DashboardLayout() {
return (
<div className="dashboard">
<DashboardSidebar />
<main>
{/* The Outlet component renders the matched child route element */}
<Outlet />
</main>
</div>
);
}
export default DashboardLayout;

Protected Routes (Navigate)

Redirect users based on authentication status.

PrivateRoute.jsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from './auth'; // Assume a custom hook for auth status
function PrivateRoute({ children }) {
const { isAuthenticated } = useAuth();
const location = useLocation();
if (!isAuthenticated) {
// Redirect them to the /login page, but save the current location they were
// trying to go to. This allows us to send them back after login.
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children; // Render the protected component
}
export default PrivateRoute;
Route Setup
// In your Routes definition:
<Route
path="/admin"
element={
<PrivateRoute>
<AdminDashboard />
</PrivateRoute>
}
/>
<Route path="/login" element={<LoginPage />} />

Programmatic Navigation (useNavigate)

Navigate imperatively from within your component logic (e.g., after a form submission).

LoginForm.jsx
import { useNavigate } from 'react-router-dom';
import { useAuth } from './auth';
function LoginForm() {
const navigate = useNavigate();
const { login } = useAuth();
const location = useLocation();
const from = location.state?.from?.pathname || "/"; // Redirect back or to home
const handleSubmit = async (event) => {
event.preventDefault();
// ... get form data ...
const success = await login(formData); // Assume login returns true/false
if (success) {
navigate(from, { replace: true }); // Navigate programmatically
} else {
// Handle login error
}
};
return <form onSubmit={handleSubmit}>{/* ... form fields ... */}</form>;
}

Query Parameters (useSearchParams)

Read and modify query parameters in the URL (e.g., /search?q=react&sort=asc).

SearchPage.jsx
import { useSearchParams } from 'react-router-dom';
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q') || ''; // Get 'q' parameter
const sort = searchParams.get('sort') || 'relevance'; // Get 'sort' parameter
const handleQueryChange = (event) => {
const newQuery = event.target.value;
// Update search params - creates new URL like /search?q=newQuery&sort=...
setSearchParams({ q: newQuery, sort });
};
const handleSortChange = (event) => {
const newSort = event.target.value;
setSearchParams({ q: query, sort: newSort });
};
return (
<div>
<input type="text" value={query} onChange={handleQueryChange} placeholder="Search..." />
<select value={sort} onChange={handleSortChange}>
<option value="relevance">Relevance</option>
<option value="date">Date</option>
<option value="price">Price</option>
</select>
<p>Showing results for "{query}" sorted by {sort}</p>
{/* Fetch and display search results based on query and sort */}
</div>
);
}

404 Not Found Page

Define a catch-all route to handle URLs that don’t match any other defined routes.

Route Setup
<Routes>
{/* ... your other routes ... */}
{/* The path="*" route MUST be the last Route defined */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
NotFoundPage.jsx
function NotFoundPage() {
return (
<div>
<h1>404 - Page Not Found</h1>
<p>Sorry, the page you are looking for does not exist.</p>
<Link to="/">Go back home</Link>
</div>
);
}

Example: React Router Demo

Explore a live example demonstrating various React Router features.


Context API

React’s built-in Context API provides a way to pass data down the component tree without having to pass props manually at every level. It’s designed to share data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language.

When is Context Appropriate?

  • Managing UI themes (e.g., dark/light mode).
  • Sharing user authentication status and data.
  • Handling application-wide language or locale settings.
  • Sharing data required by many components at different nesting levels.
  • When prop drilling becomes cumbersome (passing props down 3+ levels).

Basic Context Usage

  1. Create Context: Use createContext to create a Context object. You can provide an optional default value.

    UserContext.js
    import { createContext } from 'react';
    // The default value (null here) is used only when a component does not have a matching Provider above it in the tree.
    export const UserContext = createContext(null);
  2. Provide Context: Wrap the part of your component tree that needs access to the context data with the Context Provider. Pass the shared data via the value prop.

    App.jsx
    import { UserContext } from './UserContext';
    import MainContent from './MainContent';
    function App() {
    const currentUser = { name: 'Alice', id: 123 };
    return (
    // Any component inside this Provider can access the 'currentUser' value
    <UserContext.Provider value={currentUser}>
    <h1>My App</h1>
    <MainContent />
    </UserContext.Provider>
    );
    }
    export default App;
  3. Consume Context: Use the useContext hook within any functional component nested under the Provider to read the context value.

    UserProfile.jsx
    import React, { useContext } from 'react';
    import { UserContext } from './UserContext';
    function UserProfile() {
    // useContext reads the current value from the nearest UserContext.Provider
    const user = useContext(UserContext);
    return (
    <div>
    <h2>User Profile</h2>
    {user ? <p>Welcome, {user.name}!</p> : <p>Please log in.</p>}
    </div>
    );
    }
    export default UserProfile;

Context with State Management

Often, you’ll want the context value to be dynamic. You can combine Context with useState (or useReducer) in the Provider component.

AuthContext.js
import React, { createContext, useState, useContext } from 'react';
// 1. Create Context
const AuthContext = createContext(null);
// 2. Create Provider Component
export function AuthProvider({ children }) {
const [user, setUser] = useState(null); // Manage auth state here
const login = (userData) => {
// In reality, call an API, then setUser
setUser(userData);
console.log("User logged in:", userData);
};
const logout = () => {
setUser(null);
console.log("User logged out");
};
// The value passed includes both the state and the functions to change it
const value = { user, login, logout };
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// 3. Custom Hook for easier consumption (optional but recommended)
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
App.jsx (Using AuthProvider)
import { AuthProvider } from './AuthContext';
import LoginButton from './LoginButton';
import UserStatus from './UserStatus';
function App() {
return (
<AuthProvider> {/* Wrap the relevant part of the app */}
<h1>App with Auth</h1>
<UserStatus />
<LoginButton />
</AuthProvider>
);
}
export default App;
LoginButton.jsx (Consuming via Custom Hook)
import { useAuth } from './AuthContext';
function LoginButton() {
const { user, login, logout } = useAuth(); // Use the custom hook
return (
<button onClick={() => user ? logout() : login({ name: 'Bob' })}>
{user ? 'Log Out' : 'Log In'}
</button>
);
}
export default LoginButton;

Multiple Contexts

You can nest multiple Providers to share different types of global data.

import { ThemeProvider } from './ThemeContext'; // Assume similar setup as AuthContext
import { AuthProvider } from './AuthContext';
function App() {
return (
<AuthProvider>
<ThemeProvider>
{/* Components inside can access both auth and theme contexts */}
<Layout />
</ThemeProvider>
</AuthProvider>
);
}

Performance Optimization

Optimization Strategies:

  1. Split Contexts: Separate frequently changing data from stable functions or less frequently changing data into different contexts.

    // Split user data and actions
    const UserDataContext = createContext();
    const UserActionsContext = createContext();
    function UserProvider({ children }) {
    const [user, setUser] = useState(null);
    const actions = { login: ..., logout: ... }; // Assume stable actions
    return (
    <UserDataContext.Provider value={user}>
    <UserActionsContext.Provider value={actions}>
    {children}
    </UserActionsContext.Provider>
    </UserDataContext.Provider>
    );
    }
    // Components can now consume only UserActionsContext if they only need actions.
  2. Memoization (useMemo): If the context value is an object or array created within the Provider component, wrap its creation in useMemo to ensure its reference only changes when its dependencies actually change.

    function ThemeProvider({ children }) {
    const [isDark, setIsDark] = useState(false);
    const toggleTheme = useCallback(() => setIsDark(!isDark), []);
    // Memoize the context value object
    const themeValue = useMemo(() => ({
    isDark,
    colors: { /* ... derive colors based on isDark ... */ },
    toggleTheme
    }), [isDark, toggleTheme]); // Only changes if isDark or toggleTheme changes
    return (
    <ThemeContext.Provider value={themeValue}>
    {children}
    </ThemeContext.Provider>
    );
    }

State Management Evolution: Task Manager Example

Let’s trace how state management might evolve in a hypothetical “Task Manager” application, highlighting the pros and cons of different approaches.

1. Initial Approach: Prop Drilling

  • Code Example: Task Manager (Props) (Conceptual Link)
  • Description: All state (tasks, filter, etc.) is held in the top-level App component. State and functions to update state are passed down through props to child components.

Problems Encountered:

  1. Prop Drilling: State and setters needed by deeply nested components (like TaskItem) have to be passed through intermediate components (TaskList) that don’t use them directly. This makes the code verbose and harder to refactor.
    // App -> TaskList -> TaskItem
    // App passes `setTasks`, `setHistory` down to TaskList
    // TaskList passes `setTasks`, `setHistory` down to TaskItem
    // TaskList doesn't actually *use* these props.
  2. Component Coupling: Components become tightly coupled. Changing how state is managed in App might require changes in many intermediate components.
  3. Re-rendering: Any state change in App causes App and potentially its entire subtree to re-render, even if only a small part of the UI needs updating. useMemo can help but doesn’t solve re-renders caused by prop reference changes.
  4. State Synchronization: Logic for updating related state (e.g., tasks and taskHistory) might be spread across different event handlers in various components, making it harder to keep consistent.

2. Improvement: Context API

  • Code Example: Task Manager (Context API) (Conceptual Link)
  • Description: A TaskProvider component is created using createContext and useState. It holds the tasks, filter, taskHistory, etc., state and provides functions to update them. Components like TaskItem or TaskFilters use useContext (or a custom useTaskContext hook) to directly access the needed state or actions.

Problems Solved:

  1. Eliminated Prop Drilling: Components access required state/actions directly from the context, regardless of their depth in the tree.
    // TaskItem directly uses useTaskContext()
    function TaskItem({ task }) {
    const { updateTask, deleteTask } = useTaskContext();
    // No need for props from TaskList
    }
  2. Centralized State Logic: State and the primary functions to modify it are co-located within the TaskProvider, making updates more manageable.
  3. Improved State Synchronization: Functions like updateTask within the provider can handle updating both tasks and taskHistory consistently in one place.

New Challenges / Context API Limitations:

  1. Performance Issues at Scale: By default, any component consuming the context re-renders whenever any part of the context value changes. If TaskContext.Provider’s value includes tasks, filter, and searchTerm, updating just the searchTerm will still cause components only interested in tasks (like TaskStats) to re-render unnecessarily. Optimization techniques (splitting contexts, memoization) are needed but add complexity.
  2. Context Value Complexity: As the application grows, the single context value object can become very large, making it harder to manage and understand dependencies.
  3. Debugging: While better than prop drilling, tracing why a component re-rendered due to context changes can still be tricky without specialized tools. There’s no built-in time-travel debugging or action logging like in dedicated state management libraries.

3. Dedicated State Management: Redux Toolkit

  • Code Example: Task Manager (Redux Toolkit) (Conceptual Link)
  • Description: Redux Toolkit (RTK) provides a more structured and opinionated way to manage global state, designed to address the complexities and performance issues that can arise with Context API in large applications.

Core Concepts:

  1. Store: A single, centralized store holds the entire application state.
    store/store.js
    import { configureStore } from '@reduxjs/toolkit';
    import tasksReducer from './tasksSlice'; // Import the slice reducer
    export const store = configureStore({
    reducer: {
    // Define reducers for different parts of the state
    tasks: tasksReducer,
    // user: userReducer, // Example for other state slices
    },
    });
  2. Slice: A collection of Redux reducer logic and actions for a single feature or domain (e.g., tasks). createSlice simplifies this.
    store/tasksSlice.js
    import { createSlice } from '@reduxjs/toolkit';
    const initialState = {
    tasks: [],
    taskHistory: [],
    filter: 'all',
    searchTerm: '',
    };
    const tasksSlice = createSlice({
    name: 'tasks', // Slice name
    initialState,
    // Reducers define how state can be updated
    reducers: {
    addTask: (state, action) => {
    state.tasks.push(action.payload); // RTK uses Immer for immutable updates
    // Add to history implicitly or explicitly here
    },
    updateTask: (state, action) => {
    const { id, updates } = action.payload;
    const task = state.tasks.find(t => t.id === id);
    if (task) { Object.assign(task, updates); }
    // Add to history
    },
    // ... other reducers: deleteTask, setFilter, setSearchTerm
    },
    });
    // Export actions automatically generated by createSlice
    export const { addTask, updateTask, deleteTask, setFilter, setSearchTerm } = tasksSlice.actions;
    // Export the reducer
    export default tasksSlice.reducer;
  3. Actions: Plain JavaScript objects describing what happened (e.g., { type: 'tasks/addTask', payload: { id: 1, text: '...' } }). RTK’s createSlice automatically generates action creators.
  4. Reducers: Pure functions that take the previous state and an action, and return the next state (previousState, action) => newState. They specify how the state changes in response to actions. RTK uses Immer internally, allowing you to write “mutating” logic within reducers that gets translated into safe, immutable updates.
  5. Dispatch: How you send actions to the store to trigger state updates (dispatch(addTask(newTask))).
  6. Selector: How components read data from the store (const tasks = useSelector(state => state.tasks.tasks)). React-Redux optimizes re-renders based on selector results.

Key Improvements Over Context API:

  1. Predictable State Flow: Unidirectional data flow (Dispatch Action -> Reducer Updates State -> UI Re-renders) makes state changes easier to trace.
  2. Performance Optimization: useSelector allows components to subscribe to specific pieces of the state. Components only re-render if the data they selected actually changes, avoiding the unnecessary re-renders common with basic Context API usage.
  3. Debugging Power: Excellent integration with Redux DevTools allows time-travel debugging, action logging, and state inspection.
  4. Structure & Scalability: Provides a clear pattern for organizing state logic, especially beneficial in large applications with multiple developers.
  5. Middleware: Easily add middleware for handling side effects (like API calls with RTK Query or Redux Thunk), logging, etc.

When to Consider Redux Toolkit:

  • Large applications with complex, shared state.
  • When fine-grained performance optimization of re-renders is crucial.
  • When advanced debugging capabilities (like time-travel) are needed.
  • When a strict, predictable state management pattern is desired for team collaboration.