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):
- A user receives new messages.
- A notification badge appears on the chat icon.
- The user opens the chat and reads the messages.
- 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.
<!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.
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.
npm create vite@latest your-project-name --template react-ts# Follow prompts: select React -> TypeScript (or JavaScript)cd your-project-namenpm installnpm 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
, formerlyreact-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.
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):
// 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)
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()
.
const element = <h1 className="greeting">Hello, world!</h1>;
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:
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):- Cleanup function from the previous render runs (if the effect ran before).
- 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 Contextconst 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 Contextfunction 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).
```jsxfunction 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.memoconst ChildComponent = React.memo(({ onClick }) => { console.log("Child rendered"); return <button onClick={onClick}>Click Child</button>;});
Example: Password Generator (Using Hooks)
// Import necessary hooks and functions from React.import React, { useState, useCallback, useEffect, useRef } from 'react';
// Character sets for password generationconst CHAR_SETS = { /* ... as defined before ... */ };
// Main functional componentfunction 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.).
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;
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.
- 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. - Only Call Hooks from React Functions: Call Hooks from React functional components or other custom Hooks. Don’t call them from regular JavaScript functions.
- (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
npm install react-router-dom
Basic Routing Setup
Wrap your application with BrowserRouter
and define routes using Routes
and Route
.
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>);
Navigation (Link
and NavLink
)
Use Link
for basic navigation and NavLink
when you need to style the active link.
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.
// In your Routes definition:<Route path="/users/:userId" element={<UserProfile />} /><Route path="/products/:category/:productId" element={<ProductDetail />} />
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.
// ... importsimport 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>
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.
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;
// 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).
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
).
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.
<Routes> {/* ... your other routes ... */}
{/* The path="*" route MUST be the last Route defined */} <Route path="*" element={<NotFoundPage />} /></Routes>
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
-
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); -
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 thevalue
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; -
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.Providerconst 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.
import React, { createContext, useState, useContext } from 'react';
// 1. Create Contextconst AuthContext = createContext(null);
// 2. Create Provider Componentexport 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;}
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;
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 AuthContextimport { AuthProvider } from './AuthContext';
function App() { return ( <AuthProvider> <ThemeProvider> {/* Components inside can access both auth and theme contexts */} <Layout /> </ThemeProvider> </AuthProvider> );}
Performance Optimization
Optimization Strategies:
-
Split Contexts: Separate frequently changing data from stable functions or less frequently changing data into different contexts.
// Split user data and actionsconst UserDataContext = createContext();const UserActionsContext = createContext();function UserProvider({ children }) {const [user, setUser] = useState(null);const actions = { login: ..., logout: ... }; // Assume stable actionsreturn (<UserDataContext.Provider value={user}><UserActionsContext.Provider value={actions}>{children}</UserActionsContext.Provider></UserDataContext.Provider>);}// Components can now consume only UserActionsContext if they only need actions. -
Memoization (
useMemo
): If the context value is an object or array created within the Provider component, wrap its creation inuseMemo
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 objectconst themeValue = useMemo(() => ({isDark,colors: { /* ... derive colors based on isDark ... */ },toggleTheme}), [isDark, toggleTheme]); // Only changes if isDark or toggleTheme changesreturn (<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-levelApp
component. State and functions to update state are passed down through props to child components.
Problems Encountered:
- 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. - Component Coupling: Components become tightly coupled. Changing how state is managed in
App
might require changes in many intermediate components. - Re-rendering: Any state change in
App
causesApp
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. - State Synchronization: Logic for updating related state (e.g.,
tasks
andtaskHistory
) 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 usingcreateContext
anduseState
. It holds thetasks
,filter
,taskHistory
, etc., state and provides functions to update them. Components likeTaskItem
orTaskFilters
useuseContext
(or a customuseTaskContext
hook) to directly access the needed state or actions.
Problems Solved:
- 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}
- Centralized State Logic: State and the primary functions to modify it are co-located within the
TaskProvider
, making updates more manageable. - Improved State Synchronization: Functions like
updateTask
within the provider can handle updating bothtasks
andtaskHistory
consistently in one place.
New Challenges / Context API Limitations:
- Performance Issues at Scale: By default, any component consuming the context re-renders whenever any part of the context
value
changes. IfTaskContext.Provider
’s value includestasks
,filter
, andsearchTerm
, updating just thesearchTerm
will still cause components only interested intasks
(likeTaskStats
) to re-render unnecessarily. Optimization techniques (splitting contexts, memoization) are needed but add complexity. - Context Value Complexity: As the application grows, the single context
value
object can become very large, making it harder to manage and understand dependencies. - 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:
- 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 reducerexport const store = configureStore({reducer: {// Define reducers for different parts of the statetasks: tasksReducer,// user: userReducer, // Example for other state slices},}); - 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 nameinitialState,// Reducers define how state can be updatedreducers: {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 createSliceexport const { addTask, updateTask, deleteTask, setFilter, setSearchTerm } = tasksSlice.actions;// Export the reducerexport default tasksSlice.reducer; - Actions: Plain JavaScript objects describing what happened (e.g.,
{ type: 'tasks/addTask', payload: { id: 1, text: '...' } }
). RTK’screateSlice
automatically generates action creators. - 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. - Dispatch: How you send actions to the store to trigger state updates (
dispatch(addTask(newTask))
). - 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:
- Predictable State Flow: Unidirectional data flow (Dispatch Action -> Reducer Updates State -> UI Re-renders) makes state changes easier to trace.
- 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. - Debugging Power: Excellent integration with Redux DevTools allows time-travel debugging, action logging, and state inspection.
- Structure & Scalability: Provides a clear pattern for organizing state logic, especially beneficial in large applications with multiple developers.
- 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.