Routing in web applications allows for seamless navigation, ensuring users effortlessly access different pages and resources within an application. As such, when implemented correctly, routing offers an intuitive user experience that enhances interactions. By default, React ships without built-in routing capabilities, which is why developers rely on React Router – a third-party client-side routing library for React applications.
React Router follows a declarative approach to defining routes, meaning it lets you declare exactly which component of your application will be displayed for a certain route. This way, you can define intuitive and logical routes that can be modified without affecting other parts of the application.
At its core, react routing allows a single-page application (SPA) to simulate a multipage experience – without requiring a full page reload. This enables you to build complex applications with reduced server load, unlike multi-page applications. When a user navigates to a certain route, React Router dynamically loads and renders the appropriate component without requiring the server to generate an entire page for each request. This offers a smooth user experience since only the necessary parts of the UI are updated.
Traditional multi-page web applications use server-based routing and typically consist of a large number of view files. This means that every time a user requests a new page, the request has to be sent to the server. The server then responds by generating and sending back the requested page. As the user interacts with the page, the browser sends requests to the server again resulting in full page reloads, which negatively impact the user experience.
SPAs, on the other hand, use client-side routing and usually load a single HTML page. When a user navigates to a different route, React Router intercepts the URL and updates the browser history without sending a request to the server. The page is then dynamically updated with the corresponding component based on the route. When the user interacts with the page, only the necessary components are updated without requiring a full page reload. This results in a smooth user experience and faster page loading times.
Additionally, thanks to React Router’s client-side routing, SPAs boast reduced server load. The only communication to the server is through APIs to facilitate data retrieval. This decouples the backend from the frontend, allowing for a more modular and flexible codebase. Also, with a decoupled architecture, it becomes easier for developers to collaborate since they can work on the backend and the frontend simultaneously.
React Router offers components and hooks for managing a user’s navigation history. These core components include:
The BrowserRouter component is the backbone of React Router, responsible for keeping your UI in sync with the URL. This means that any changes to the URL will result in the corresponding component being rendered, and any navigation actions (like clicking a link or programmatically navigating) will update the URL accordingly.
Under the hood, the component uses HTML5 history API – pushState, replaceState, and popState – to monitor and manipulate the app’s browsing history. Typically, you wrap your entire application in the BrowserRouter component, to give React Router access and control of your app’s location.
import ReactDOM from 'react-dom';
import * as React from 'react';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('app')
);
In this example, the BrowserRouter component wraps the entire app enabling React Router to handle the routing for your application.
Besides BrowserRouter, you can also use MemoryRouter, HashRouter, or StaticRouter. MemoryRouter stores the history of your URL internally and does not read or write to the address bar. This is useful for scenarios where you do not need to interact with the browser’s history stack, such as testing environments. HashRouter uses the hash portion of the URL (i.e., the part after the # symbol) to keep the UI in sync with the URL. This can be useful in scenarios where you need to support older browsers that don’t support the HTML5 history API. StaticRouter is particularly useful for static site generation where the router doesn’t need any navigation but renders based on the location prop provided.
The Routes component is used to group all the app’s routes. Each Route is used to specify a path and the React component that should be rendered when the URL matches that path. For example if you want to render the Home component when a user navigates to “/”. You can define your route like this :
import { Routes, Route } from 'react-router-dom';
import Home from './Home';
import About from './About';
import Contact from './Contact';
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
);
}
export default App;
In the example above, when the URL path is “/”, the Home component will be rendered. Similarly, when the URL path is “/about” or “/contact”, the About or Contact component will be rendered, respectively.
You can also nest your routes to create a hierarchical URL structure, making it easier to manage complex navigations and layouts. For example, if you have a dashboard route, you can nest other dashboard-related routes within the parent route.
<Route path="/dashboard" element={<Dashboard />}>
<Route index element={<DashboardHome />} />
<Route path="settings" element={<DashboardSettings />} />
<Route path="profile" element={<DashboardProfile />} />
</Route>
The Link component is used to create navigation links in your application. It’s similar to the anchor HTML tags but doesn’t cause the default reload behavior when clicked.
import { Link } from 'react-router-dom';
function Navigation() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
</nav>
);
}
NavLink works in the same way as Link , but adds an active styling attribute to highlight an active link.
import { NavLink } from 'react-router-dom';
function Navigation() {
return (
<nav>
<NavLink to="/" activeClassName="active">Home</NavLink>
<NavLink to="/about" activeClassName="active">About</NavLink>
<NavLink to="/contact" activeClassName="active">Contact</NavLink>
</nav>
);
}
The useNavigate hook allows users to programmatically navigate between routes. It provides a function that you can call to change the current URL, which in turn updates the displayed component based on the route configuration. It’s particularly useful when you want to redirect a user to another page after completing an action, for example, after submitting a form.
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
const handleSubmit = () => {
// Perform login logic
navigate('/dashboard');
};
return <button onClick={handleSubmit}>Login</button>;
}
Alternatively, you can also use Navigate for a more declarative navigation approach especially for conditionally navigating based on state or props within the component’s render method.
import { Navigate } from 'react-router-dom';
function ProtectedRoute({ isAuthenticated }) {
if (isAuthenticated) {
return <Navigate to="/dashboard" />;
} else {
return <Navigate to="/login" />;
}
}
useParams hook allows users to access the dynamic parameters of a route. This is particularly useful when you need to fetch or display data based on a URL parameter, such as an ID. Essentially, the hook returns an object of key-value pairs where the keys are the parameter names and the values are the parameter values from the URL. For example, if you were to fetch a products details based on the id, here’s how you would implement it:
import { Routes, Route } from 'react-router-dom';
import Home from './Home';
import Products from './Products';
import ProductDetail from './ProductDetail';
import About from './About';
function App() {
return (
<Routes>
<Route path=”/” element={<Home />} />
<Route path="products" element={<Products />}>
<Route path=":id" element={<ProductDetail />} />
</Route>
<Route path="about" element={<About />} />
</Routes>
);
}
export default App;
Note that in the example above, the ProductDetail route is nested inside the Products route and uses a URL parameter :id to represent individual product IDs.
In the Products component, you can display the list of your products for users to navigate to a product’s details page. The Outlet component renders the children routes of the parent product route.
import { Outlet, Link } from 'react-router-dom';
function Products() {
return (
<div>
<h1>Products</h1>
<ul>
<li><Link to="1">Product 1</Link></li>
<li><Link to="2">Product 2</Link></li>
<li><Link to="3">Product 3</Link></li>
</ul>
<Outlet />
</div>
);
}
export default Products;
In the ProductDetails component,you can use the useParams hooks to extract the id parameter from the URL, and use it to fetch and display details specific to the selected product.
import { useParams } from 'react-router-dom';
function ProductDetail() {
const { id } = useParams();
return (
<div>
<h2>Product Detail</h2>
<p>Displaying details for product ID: {id}</p>
{/* Additional logic to fetch and display product details can go here */}
</div>
);
}
export default ProductDetail;
Having looked at the React Router’s fundamental components and hooks, it’s important to understand some of the best practices for effective routing. These practices will not only ensure a secure and efficient navigation experience, but also improve your application’s maintainability, scalability, and performance.
Ideally, your application should be organized into directories where all related files are stored in one directory. As such, all your route definitions should be in one directory making it easier to manage and modify routes. Most importantly, your routes should be broken down into small, reusable components to further enhance the maintainability of individual routes.
JavaScript bundles in modern applications can easily become large, which consequently impairs the application’s overall performance. This is where lazy loading comes into play, offering a solution to optimize the loading time and enhance performance of your application. It works by allowing you to split your code into smaller chunks that can be loaded on demand. This way, when a user visits your site, they don’t necessarily need to download the entire code upfront, especially for routes they have not visited and don’t need at the moment.
To implement lazy loading, you need to use React.lazy. It’s an inbuilt function that dynamically allows you to import a component only when it’s needed. When used together with React Router, it allows you to defer the loading of a component until the user visits the route that requires that specific component.
The function takes in import as an argument and returns a component. Here’s how to implement it:
import React, { Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
const Home = React.lazy(() => import('./Home'));
const About = React.lazy(() => import('./About'));
const Products = React.lazy(() => import('./Products'));
const ProductDetail = React.lazy(() => import('./ProductDetail'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="products" element={<Products />}>
<Route path=":id" element={<ProductDetail />} />
</Route>
</Routes>
</Suspense>
</Router>
);
}
export default App;
When a user visits the home page, only the Home component will be loaded. Note that the Suspense component is used to wrap the lazy loading routes. It takes in a prop fallback which accepts a React component you want to render while the lazy-loaded route is being loaded. Ideally, this should be a loading spinner or a loading message to show the user that the app is fetching the needed route.
As users navigate through your app, they may encounter errors such as trying to access a non-existent route. For this reason, effective error handling is crucial to ensure your application gracefully manages such errors to guarantee a seamless user experience.
One of the best ways to handle routing errors is implementing a fallback route to catch errors related to undefined paths. Essentially, this is known as a catch-all route that displays an error page and can also contain a navigation button that takes users to the homepage. To implement it all you have to do is define a route with a path of “*” that renders a NotFound element.
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="products" element={<Products />} />
<Route path="*" element={<NotFound />} />
</Routes>
Besides handling errors related to non-existent routes, you may also consider handling errors that occur within the app itself. This can be achieved using the ErrorBoundary component from react-error-boundary library. The component takes in a fallback component that will be displayed in case of errors.
import React, { Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';
const Home = React.lazy(() => import('./Home'));
const About = React.lazy(() => import('./About'));
const Products = React.lazy(() => import('./Products'));
const ProductDetail = React.lazy(() => import('./ProductDetail'));
const NotFound = React.lazy(() => import('./NotFound'));
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
function App() {
return (
<Router>
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// reset the state of your app so the error doesn't happen again
}}
>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="products" element={<Products />}>
<Route path=":id" element={<ProductDetail />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</ErrorBoundary>
</Router>
);
}
export default App;
This setup ensures you build a robust application that gracefully handles loading state and route errors.
React Router is the most popular routing library in the React ecosystem primarily due its extensive documentation, ease of setup, and a large community of users. It also offers a wide range of features making it ideal for complex applications. However, there are other alternatives to React Router that can equally get the job done. They include:
Tanstack Router is a powerful routing library with a strong emphasis on type safety, making it a great choice for TypeScript projects. The library also integrates well with its in-built support for data fetching that allows you to invalidate, cache, and re-fetch data. It also has unique advanced features such as search-param API that allows you to manage URL query parameters more easily than using React Router’s useParams hook.
Wouter is a lightweight routing library for React and Preact apps. It’s designed for simplicity and minimalism, and as such, has a small bundle size but comes with all the capabilities of React Router. The library makes a great choice for applications that prioritize performance thanks to its minimalistic feature. Some of its advanced features include code splitting, route transitions, and route guards.
Reach Router is similar to Wouter in that it’s simple and lightweight. It’s designed with accessibility in mind, ensuring that route changes are announced by screen readers. The only downside is that unlike React Router, Reach Router lacks an extensive integration ecosystem.
Q: What is React Router and how do I use it?
A: React Router is a third-party routing library that offers component, hooks and utility functions for implementing modern routing strategies. To get started, you need to install the library using your preferred package manager – npm install react-router-dom or yarn add react-router-dom.
Q: Can I use React Router with other frameworks besides React?
A: No, React Router is specifically designed for React apps. For other frontend frameworks like Vue and Angular, you will need their respective libraries, such as Vue Router or Angular Router.
Q:How do I protect routes and manage user access in React Router?
A: You can protect routes and manage user access by using route guards and redirects. This can be implemented by creating a higher-order component (HOC) or a custom component that checks for user authentication before rendering the desired route component. If the user is not authenticated, you can redirect them to a login page or another appropriate route.
React Router is a robust routing library for building dynamic single-page applications. Its declarative approach to routing allows developers to define routes directly within their JSX. This helps improve code maintainability and organization. It also boasts a wide range of features including nested routes, dynamic route matching, and lazy loading making it ideal for building complex SPAs. While there are other alternatives to React routing, React Router remains the most popular choice thanks to its great developer experience, advanced features, and extensive documentation.