Introduction to Basic Routing & Data Flow in SPAs
Welcome to Chapter 3! In the previous chapters, we laid the groundwork by understanding React’s component-based architecture and how to manage local component state. Now, it’s time to make our applications feel truly dynamic and responsive. Imagine clicking a link on a website and seeing the content change instantly, without the entire page reloading. This magic is largely thanks to client-side routing, a cornerstone of Single Page Applications (SPAs).
In this chapter, we’ll dive deep into how SPAs navigate between different views using React Router, the most popular routing library for React. We’ll learn how to set up routes, link between pages, and access URL parameters. Alongside routing, we’ll solidify our understanding of how data moves through our React applications, exploring essential patterns like props, component state, and the Context API. Mastering these concepts is crucial for building scalable and maintainable React applications, as they dictate how users interact with your app and how information is managed behind the scenes.
By the end of this chapter, you’ll be able to build a multi-page SPA where users can navigate seamlessly, and components can effectively share and manage their data. Get ready to transform your static React components into an interconnected, interactive application!
Core Concepts: Navigating and Communicating in Your SPA
Before we jump into code, let’s build a strong mental model for how routing and data flow work in a modern React SPA.
What is Client-Side Routing?
In traditional web applications, clicking a link or submitting a form typically sends a request to the server, which then responds with a brand new HTML page. This causes a full page reload, often perceived as a flicker or delay.
Client-Side Routing, a hallmark of Single Page Applications (SPAs), completely changes this. Instead of requesting a new HTML page from the server for every navigation, the browser’s JavaScript takes over. When you click a link:
- The JavaScript intercepts the navigation event.
- It prevents the default browser behavior (full page reload).
- It dynamically updates the URL in the browser’s address bar using the
History API(a browser feature that allows JavaScript to manipulate the browser’s session history). - It then renders the appropriate React components corresponding to the new URL, all without leaving the current HTML page.
This approach offers a much smoother, faster, and more app-like user experience.
Think of it like this: Instead of going to a new library every time you want a different book, you stay in the same reading room, and a librarian (your React app) quickly brings you the next book (component) you requested.
Introducing React Router v6
While the browser’s History API provides the low-level mechanism, using it directly can be complex. That’s where routing libraries come in. react-router-dom is the de-facto standard for routing in React applications. As of February 14, 2026, react-router-dom v6.x is the stable and recommended version, offering a modern, hooks-based API.
Here are the core components and hooks you’ll encounter:
BrowserRouter: This is the top-level component that wraps your entire application. It uses the HTML5 History API to keep your UI in sync with the URL. You typically place it in yoursrc/main.jsx(orindex.js) file.Routes: A wrapper component that groups individual<Route>elements. It’s responsible for finding the best match among its children<Route>s.Route: Defines a mapping between a URL path and a React component. When the URL matches thepathprop, the component specified by theelementprop is rendered.Link: The primary way to navigate around your application. It renders an accessible<a>tag but prevents the default full page reload, allowing React Router to handle the navigation.useNavigate: A hook that allows you to programmatically navigate (e.g., after a form submission, or clicking a “Go Back” button). It returns a function you can call with a path.useParams: A hook that lets you access URL parameters (e.g.,/products/123where123is theproductId).
Basic Data Flow in React SPAs
How do different parts of your application communicate? React promotes a unidirectional data flow, meaning data primarily flows down the component tree from parent to child.
Props (Properties):
- What: The primary mechanism for passing data from a parent component to its child components. Props are immutable within the child component.
- Why: To configure child components, pass down data to be displayed, or provide functions for children to call back to the parent.
- How: Parent passes data as attributes on the child component (e.g.,
<ChildComponent message="Hello" />). Child receives them as an argument to its function component (e.g.,function ChildComponent({ message }) { ... }). - Mental Model: Like handing a book to someone below you on a ladder. They can read it, but they can’t change the original copy.
State (Local Component State):
- What: Data that a component manages internally and can change over time. It makes a component dynamic and interactive.
- Why: To track user input, toggle UI elements, manage loading states, or store data fetched from an API that’s specific to that component’s lifecycle.
- How: Using the
useStatehook (e.g.,const [count, setCount] = useState(0);). - Mental Model: A sticky note you keep on your own desk; you can write on it and change it, but others only see the current version if you show it to them.
Context API:
- What: A way to share data (like user authentication status, theme settings, or fetched global data) across a component tree without manually passing props down through every level (known as “prop drilling”).
- Why: To avoid prop drilling when many components need access to the same global-like data.
- How: You create a
Contextobject, provide aContext.Providerhigher up in your tree with the value, and consume it with theuseContexthook in any descendant component. - Mental Model: A shared whiteboard in a meeting room. Anyone in the room can look at it or write on it (if allowed), without needing someone to physically hand them the information.
Data Fetching (Introduction):
- What: Retrieving data from external sources, typically APIs.
- Why: To display dynamic content, user-specific information, or interact with backend services.
- How: Using the browser’s built-in
fetchAPI or a library likeaxioswithin auseEffecthook. TheuseEffecthook is crucial for performing side effects like data fetching in function components, ensuring it runs at appropriate times (e.g., on component mount). - Mental Model: Ordering food from a restaurant. You send a request, wait for the response, and then display what you received.
Step-by-Step Implementation: Building a Simple Product Viewer
Let’s put these concepts into practice by building a basic product viewer application. This app will have a home page, a list of products, and individual product detail pages, all navigated client-side.
Project Setup
First, let’s set up our React project. We’ll use Vite, a modern build tool that offers a much faster development experience than create-react-app.
Create a new Vite React project: Open your terminal and run:
npm create vite@latest my-product-viewer -- --template reactnpm create vite@latest: This command usesnpmto create a new project with the latest Vite.my-product-viewer: This will be the name of our project directory.--template react: This specifies that we want a React template.
Navigate into your project folder:
cd my-product-viewerInstall dependencies:
npm installInstall
react-router-dom: We need the routing library! As of 2026-02-14,react-router-domv6.22.1is a recent stable version.npm install [email protected]Start the development server:
npm run devYour browser should open to
http://localhost:5173(or a similar port). You’ll see the default Vite + React starter page.
Step 1: Initial App Structure & Navbar
Let’s clean up our project and add a basic navigation bar.
Open
src/App.jsx: Replace its content with a clean functional component.// src/App.jsx import React from 'react'; import { Outlet } from 'react-router-dom'; // We'll use Outlet later for nested routes import Navbar from './components/Navbar'; function App() { return ( <div> <Navbar /> <div className="container"> {/* This is where our routed components will render */} <Outlet /> </div> </div> ); } export default App;- Explanation: We import
Navbar(which we’ll create next) andOutletfromreact-router-dom.Outletis a placeholder that will render the child routes ofApp.
- Explanation: We import
Create
src/componentsdirectory andNavbar.jsx: Insidesrc, create a new folder namedcomponents. Insidecomponents, createNavbar.jsx.// src/components/Navbar.jsx import React from 'react'; import { Link } from 'react-router-dom'; import './Navbar.css'; // We'll create this CSS file next function Navbar() { return ( <nav className="navbar"> <Link to="/" className="navbar-brand"> My Product Viewer </Link> <ul className="navbar-nav"> <li className="nav-item"> <Link to="/" className="nav-link"> Home </Link> </li> <li className="nav-item"> <Link to="/products" className="nav-link"> Products </Link> </li> </ul> </nav> ); } export default Navbar;- Explanation: We import
Linkfromreact-router-dom. Instead of a regular<a>tag,Linkprevents full page reloads and allows React Router to handle the navigation. Thetoprop specifies the target path.
- Explanation: We import
Create
src/components/Navbar.css: Let’s add some minimal styling for our navbar./* src/components/Navbar.css */ .navbar { background-color: #333; padding: 1rem; display: flex; justify-content: space-between; align-items: center; color: white; } .navbar-brand { color: white; text-decoration: none; font-size: 1.5rem; font-weight: bold; } .navbar-nav { list-style: none; margin: 0; padding: 0; display: flex; } .nav-item { margin-left: 1rem; } .nav-link { color: white; text-decoration: none; padding: 0.5rem 1rem; border-radius: 4px; transition: background-color 0.3s ease; } .nav-link:hover { background-color: #555; } .container { padding: 20px; max-width: 960px; margin: 20px auto; background-color: #f9f9f9; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); }Clean up
src/index.css(optional but good practice): Remove default Vite/React styles to start fresh. You can leave a basic body style./* src/index.css */ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; background-color: #eee; } code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; }
Step 2: Define Routes
Now, let’s configure react-router-dom in our main entry file.
Open
src/main.jsx: This is where we’ll set upBrowserRouterand our application routes using thecreateBrowserRouterfunction, a modern approach for React Router v6.// src/main.jsx import React from 'react'; import ReactDOM from 'react-dom/client'; import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import App from './App.jsx'; import Home from './pages/Home.jsx'; // We'll create these pages next import Products from './pages/Products.jsx'; import ProductDetail from './pages/ProductDetail.jsx'; import './index.css'; // 1. Define our routes configuration const router = createBrowserRouter([ { path: '/', // The root path element: <App />, // The App component will be rendered here children: [ // Nested routes will render inside App's <Outlet /> { index: true, // This marks the Home component as the default child route for '/' element: <Home />, }, { path: 'products', // Path for the product list (relative to '/') element: <Products />, }, { path: 'products/:productId', // Path for individual product details, with a dynamic parameter element: <ProductDetail />, }, ], }, ]); ReactDOM.createRoot(document.getElementById('root')).render( <React.StrictMode> {/* 2. Provide the router to our application */} <RouterProvider router={router} /> </React.StrictMode>, );- Explanation:
- We import
createBrowserRouterandRouterProviderfromreact-router-dom. This is the recommended way to set up routing in v6 for better performance and server-side rendering compatibility. createBrowserRoutertakes an array of route objects.- The root route (
/) uses ourAppcomponent as itselement. This meansAppwill always render, and itsOutletwill display the matchingchildrenroutes. index: trueon theHomeroute makes it the default content for the/path when no other child path matches.path: 'products/:productId'defines a dynamic segment (:productId). This allows us to capture values from the URL, likeproducts/123, where123would beproductId.- Finally,
RouterProviderwraps our entire application and makes the router available.
- We import
- Explanation:
Create
src/pagesdirectory and placeholder pages: Insidesrc, create a new folder namedpages. Insidepages, createHome.jsx,Products.jsx, andProductDetail.jsx.// src/pages/Home.jsx import React from 'react'; function Home() { return ( <div> <h1>Welcome to the Product Viewer!</h1> <p>Explore our amazing range of products.</p> </div> ); } export default Home;// src/pages/Products.jsx import React from 'react'; function Products() { return ( <div> <h1>Our Products</h1> <p>Loading products...</p> </div> ); } export default Products;// src/pages/ProductDetail.jsx import React from 'react'; function ProductDetail() { return ( <div> <h1>Product Detail</h1> <p>Loading product details...</p> </div> ); } export default ProductDetail;Now, if you refresh your browser, you should see the Navbar and the “Welcome to the Product Viewer!” message. Clicking “Products” will take you to
/productsand show “Our Products”. The routing is working!
Step 3: Displaying Product List
Let’s make our Products page display a list of mock products.
Update
src/pages/Products.jsx: We’ll create some dummy product data and render it. Each product will have a link to its detail page.// src/pages/Products.jsx import React from 'react'; import { Link } from 'react-router-dom'; // Mock product data const products = [ { id: '1', name: 'Laptop Pro', price: 1200, description: 'Powerful laptop for professionals.' }, { id: '2', name: 'Wireless Mouse', price: 25, description: 'Ergonomic mouse with long battery life.' }, { id: '3', name: 'Mechanical Keyboard', price: 90, description: 'Clicky keys for the best typing experience.' }, { id: '4', name: '4K Monitor', price: 350, description: 'Stunning visuals for work and play.' }, ]; function Products() { return ( <div> <h1>Our Products</h1> <div className="product-list"> {products.map((product) => ( <div key={product.id} className="product-card"> <h2>{product.name}</h2> <p>${product.price}</p> {/* Link to the individual product detail page */} <Link to={`/products/${product.id}`} className="view-details-link"> View Details </Link> </div> ))} </div> </div> ); } export default Products;- Explanation: We map over our
productsarray. For each product, we render its name and price. Crucially, we useLink to={/products/${product.id}}to create a dynamic link. When clicked, this will navigate to a URL like/products/1or/products/2.
- Explanation: We map over our
Add some basic styling for products (e.g., in
src/index.cssor a newProducts.css):/* Add to src/index.css or a new src/pages/Products.css */ .product-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-top: 20px; } .product-card { background-color: white; border: 1px solid #ddd; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); display: flex; flex-direction: column; justify-content: space-between; } .product-card h2 { margin-top: 0; color: #333; } .product-card p { color: #666; font-size: 0.95rem; } .view-details-link { display: inline-block; margin-top: 15px; padding: 8px 15px; background-color: #007bff; color: white; text-decoration: none; border-radius: 5px; align-self: flex-start; transition: background-color 0.2s ease; } .view-details-link:hover { background-color: #0056b3; }Refresh your browser, go to the Products page, and you should see the list. Click on any product’s “View Details” link. The URL will change, but the content will still say “Loading product details…” This is because our
ProductDetailcomponent isn’t yet using theproductIdfrom the URL.
Step 4: Product Detail Page & useParams
Now, let’s make the ProductDetail component dynamic.
Update
src/pages/ProductDetail.jsx: We’ll use theuseParamshook to get theproductIdfrom the URL and then display the corresponding product’s information. We’ll also add a “Go Back” button usinguseNavigate.// src/pages/ProductDetail.jsx import React from 'react'; import { useParams, useNavigate } from 'react-router-dom'; // Re-use the mock product data const products = [ { id: '1', name: 'Laptop Pro', price: 1200, description: 'Powerful laptop for professionals.', details: '16GB RAM, 512GB SSD, Intel i7' }, { id: '2', name: 'Wireless Mouse', price: 25, description: 'Ergonomic mouse with long battery life.', details: 'Bluetooth 5.0, 1000 DPI, 2-year warranty' }, { id: '3', name: 'Mechanical Keyboard', price: 90, description: 'Clicky keys for the best typing experience.', details: 'Cherry MX Blue switches, RGB backlight, full-size' }, { id: '4', name: '4K Monitor', price: 350, description: 'Stunning visuals for work and play.', details: '27-inch IPS, 3840x2160 resolution, USB-C hub' }, ]; function ProductDetail() { const { productId } = useParams(); // Get the productId from the URL const navigate = useNavigate(); // Get the navigate function // Find the product based on the ID const product = products.find((p) => p.id === productId); if (!product) { return ( <div> <h1>Product Not Found</h1> <p>The product with ID "{productId}" does not exist.</p> <button onClick={() => navigate('/products')}>Go to Products</button> </div> ); } return ( <div className="product-detail-card"> <h1>{product.name}</h1> <p className="product-price">${product.price}</p> <p>{product.description}</p> <p><strong>Details:</strong> {product.details}</p> <button onClick={() => navigate(-1)} className="go-back-button"> Go Back </button> </div> ); } export default ProductDetail;- Explanation:
useParams()returns an object where keys are the parameter names defined in theRoute(:productId) and values are the actual values from the URL. We destructureproductIdfrom it.useNavigate()returns a function that allows us to navigate programmatically.navigate(-1)is a special value that navigates one step back in the browser’s history, similar to clicking the browser’s back button.- We then use
productIdto find the corresponding product in our mock data. - If no product is found, we display a “Product Not Found” message and offer a button to go back to the product list.
- Explanation:
Add some basic styling for product detail (e.g., in
src/index.cssor a newProductDetail.css):/* Add to src/index.css or a new src/pages/ProductDetail.css */ .product-detail-card { background-color: white; border: 1px solid #ddd; border-radius: 8px; padding: 30px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); margin-top: 20px; } .product-detail-card h1 { color: #333; margin-bottom: 10px; } .product-price { font-size: 1.5rem; font-weight: bold; color: #007bff; margin-bottom: 20px; } .product-detail-card p { line-height: 1.6; color: #555; } .go-back-button { margin-top: 25px; padding: 10px 20px; background-color: #6c757d; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 1rem; transition: background-color 0.2s ease; } .go-back-button:hover { background-color: #5a6268; }Now, try navigating to a product detail page! The content should update correctly based on the URL parameter.
Step 5: Basic Data Fetching (Simulated)
In a real application, product data wouldn’t be hardcoded. Let’s simulate data fetching using useState and useEffect to introduce the concept of loading states.
Update
src/pages/ProductDetail.jsxagain: We’ll move theproductsdata inside the component and simulate an asynchronous fetch.// src/pages/ProductDetail.jsx import React, { useState, useEffect } from 'react'; // Import useState and useEffect import { useParams, useNavigate } from 'react-router-dom'; // Mock product data (could be from an API) const mockProducts = [ { id: '1', name: 'Laptop Pro', price: 1200, description: 'Powerful laptop for professionals.', details: '16GB RAM, 512GB SSD, Intel i7' }, { id: '2', name: 'Wireless Mouse', price: 25, description: 'Ergonomic mouse with long battery life.', details: 'Bluetooth 5.0, 1000 DPI, 2-year warranty' }, { id: '3', name: 'Mechanical Keyboard', price: 90, description: 'Clicky keys for the best typing experience.', details: 'Cherry MX Blue switches, RGB backlight, full-size' }, { id: '4', name: '4K Monitor', price: 350, description: 'Stunning visuals for work and play.', details: '27-inch IPS, 3840x2160 resolution, USB-C hub' }, ]; function ProductDetail() { const { productId } = useParams(); const navigate = useNavigate(); const [product, setProduct] = useState(null); // State to store the fetched product const [loading, setLoading] = useState(true); // State to track loading status const [error, setError] = useState(null); // State to track errors useEffect(() => { // Simulate an API call const fetchProduct = async () => { setLoading(true); setError(null); try { // Simulate network delay await new Promise((resolve) => setTimeout(resolve, 500)); const foundProduct = mockProducts.find((p) => p.id === productId); if (foundProduct) { setProduct(foundProduct); } else { setError('Product not found.'); } } catch (err) { setError('Failed to fetch product data.'); console.error(err); } finally { setLoading(false); } }; fetchProduct(); }, [productId]); // Re-run effect when productId changes if (loading) { return ( <div className="product-detail-card"> <p>Loading product details...</p> </div> ); } if (error) { return ( <div className="product-detail-card"> <h1>Error</h1> <p>{error}</p> <button onClick={() => navigate('/products')}>Go to Products</button> </div> ); } if (!product) { // Should not happen if error is caught, but as a fallback return ( <div className="product-detail-card"> <h1>Product Not Found</h1> <p>The product with ID "{productId}" does not exist.</p> <button onClick={() => navigate('/products')}>Go to Products</button> </div> ); } return ( <div className="product-detail-card"> <h1>{product.name}</h1> <p className="product-price">${product.price}</p> <p>{product.description}</p> <p><strong>Details:</strong> {product.details}</p> <button onClick={() => navigate(-1)} className="go-back-button"> Go Back </button> </div> ); } export default ProductDetail;- Explanation:
- We introduce
useStateforproduct,loading, anderrorto manage the component’s state during data fetching. - The
useEffecthook is used to perform thefetchProductside effect. It runs after the initial render and wheneverproductIdchanges (specified in the dependency array[productId]). - Inside
fetchProduct, we simulate an asynchronous operation withsetTimeoutand find the product. - We update
loadinganderrorstates accordingly. - Before rendering the product details, we check
loadinganderrorstates to display appropriate messages to the user. This is a common pattern for handling asynchronous data.
- We introduce
- Explanation:
Now, when you navigate to a product detail page, you’ll briefly see “Loading product details…” before the actual content appears, simulating a real network request.
Mini-Challenge: Add a “Contact Us” Page
Your turn! Let’s extend our product viewer application.
Challenge: Add a new “Contact Us” page to the application.
- Create a new functional component for
Contact.jsxin thesrc/pagesdirectory. This component should display a simple heading like “Contact Us” and a paragraph. - Add a new route for
/contactinsrc/main.jsxthat renders yourContactcomponent. - Add a new
Linkto the “Contact Us” page in yoursrc/components/Navbar.jsx.
Hint:
- You can copy one of the existing placeholder page components (like
Home.jsx) as a starting point forContact.jsx. - Remember to import your new
Contactcomponent insrc/main.jsxbefore adding it to thecreateBrowserRouterconfiguration.
What to observe/learn: This exercise reinforces your understanding of how to add new routes and components, and how to integrate them into the navigation system of your SPA. You’ll see how easily react-router-dom scales to include more views.
Common Pitfalls & Troubleshooting
Even with simple routing and data flow, you might encounter some common issues:
“Nothing renders when I click a link!”
- Pitfall: Forgetting to wrap your entire application (or at least the part that uses routing) with
RouterProvider(orBrowserRouterin older setups) insrc/main.jsx. React Router needs this context to function. - Troubleshooting: Double-check
src/main.jsxto ensureRouterProvider router={router}is correctly rendering yourAppcomponent.
- Pitfall: Forgetting to wrap your entire application (or at least the part that uses routing) with
"
useParamsreturnsundefined"- Pitfall: Mismatch between the parameter name in your
Routepath and the name you’re destructuring fromuseParams. For example, if your route ispath: 'products/:prodId', but you try toconst { productId } = useParams();. - Troubleshooting: Ensure the colon-prefixed name in your
pathprop (e.g.,:productId) exactly matches the key you’re trying to access from the object returned byuseParams.
- Pitfall: Mismatch between the parameter name in your
“Full page reload when clicking a link”
- Pitfall: Using a regular
<a>HTML tag instead of theLinkcomponent fromreact-router-dom. Standard<a>tags trigger a full browser navigation. - Troubleshooting: Replace all instances of
<a href="...">with<Link to="...">for internal navigation.
- Pitfall: Using a regular
Prop Drilling:
- Pitfall: Passing the same prop through many layers of components, even if intermediate components don’t directly use it. This makes your component tree rigid and hard to refactor.
- Troubleshooting: While not a “pitfall” in terms of breaking your app, it’s an architectural anti-pattern. If you find yourself passing a prop more than 2-3 levels deep, consider using React’s Context API or a dedicated state management library (like Redux, Zustand, Jotai, Recoil) for that piece of data. We’ll explore these in later chapters!
Summary
Congratulations! You’ve successfully navigated the basics of client-side routing and fundamental data flow in React SPAs. Here’s a quick recap of what we covered:
- Client-Side Routing: How SPAs provide a seamless user experience by dynamically updating content without full page reloads, leveraging the browser’s
History API. - React Router v6: We learned to use
createBrowserRouterandRouterProviderto set up our routing configuration. - Core Routing Components & Hooks:
Linkfor declarative navigation.RoutesandRouteto define URL-to-component mappings, including dynamic parameters like:productId.useParamsto extract dynamic values from the URL.useNavigatefor programmatic navigation.Outletfor rendering nested routes.
- Basic Data Flow: We reinforced our understanding of:
- Props: Passing immutable data from parent to child.
- State: Managing mutable, component-specific data using
useState. - Context API: Briefly introduced as a solution for sharing data across the component tree to avoid prop drilling.
- Data Fetching: Simulated asynchronous data loading using
useEffectanduseStateto manage loading and error states.
You now have the foundational knowledge to build multi-page React applications with interactive navigation and responsive data handling. In the next chapter, we’ll expand on data management, diving deeper into advanced state patterns and exploring how to manage complex application state more effectively.
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.