Navigating the Evolution of React Router: A Developer's Journey
As a developer returning to React after a five-year hiatus, I was struck by the significant changes in the ecosystem. One area that particularly caught my attention was React Router, a fundamental library for handling navigation in React applications. In this post, I'll share my insights on how React Router has evolved and why it's more powerful than ever.
A Trip Down Memory Lane
Cast your mind back to 2019. If you were working with React Router then, you might remember syntax that looked something like this:
import { BrowserRouter, Switch, Route } from "react-router-dom";
<BrowserRouter>
<Switch>
<Route path="/" component={Home} exact={true} />
<Route path="/signin" component={Signin} exact={true} />
<Route path="/signup" component={Signup} exact={true} />
</Switch>
</BrowserRouter>
This setup worked, but it had its limitations. The Switch
component ensured that only one route would render at a time, and the exact
prop was necessary to prevent partial matches.
Fast Forward to 2024
Fast forward to today, and React Router has undergone a significant transformation. The latest versions (v6+) have introduced a more intuitive and flexible API. Let's look at a modern example:
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/">
<Route index element={<Home />} />
<Route path="signin" element={<Signin />} />
<Route path="signup/:id" element={<Signup />} />
<Route path="*" element={<h1>Page not found</h1>} />
</Route>
)
);
The New Way of Creating Routers
One of the most noticeable changes is the introduction of createBrowserRouter
. This function provides an alternative to the <BrowserRouter>
component we used in previous versions. Instead of wrapping our routes, it takes them as parameters:
jsxCopyimport { createBrowserRouter, createRoutesFromElements, Route } from 'react-router-dom';
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<Root />}>
<Route path="home" element={<Home />} />
<Route path="about" element={<About />} />
</Route>
)
);
The createRoutesFromElements
function allows us to define our routes using JSX syntax, which many developers find more intuitive.
From Switch to Routes
We've bid farewell to the <Switch>
component and welcomed <Routes>
. This isn't just a name change; it's a fundamental shift in how routes are matched:
<Routes>
<Route path="/" element={<Home />} />
<Route path="signin" element={<Signin />} />
<Route path="signup" element={<Signup />}/>
</Routes>
<Routes>
automatically selects the best route for the current URL, eliminating the need for the exact
prop and making route matching more intelligent.
Simplified Access to Route Parameters
Gone are the days of manually passing route params through props. React Router now provides a useParams
hook, making it incredibly easy to access URL parameters:
import { useParams } from 'react-router-dom';
function MedicineDetails() {
const { id } = useParams();
return <h1>Medicine: {id}</h1>;
}
Enhanced Nested Routes
Nested routes have become more intuitive and powerful. They allow for better organization of components and routes:
<Route path="users" element={<Users />}>
<Route path=":id" element={<Medicines />} />
</Route>
This structure creates a hierarchy where child routes are rendered within their parent components.
Introducing Protected Routes
In modern web applications, controlling access to different parts of your app is crucial. React Router provides elegant solutions for implementing both public and private routes:
Public Routes These are routes that are accessible to all users without any restrictions. They typically include pages like the home page, or sign-in page. No authentication or validation is required to access these routes.
Private Routes Also known as protected routes, these are sections of your application that require authentication or meet specific conditions before access is granted. Examples might include a user dashboard, profile settings, or admin panels.
To implement private routes, we create a custom component, often named ProtectedRoute.jsx
. This component acts as a gatekeeper, validating the user's authentication status before allowing access to the protected content.
Here's how the ProtectedRoute
component typically works:
It checks for an authentication token in the browser's local storage.
If the token exists (indicating the user is logged in), it allows access to the protected route.
If no token is found, it redirects the user to the sign-in page.
This approach provides a clean and centralized way to manage access control across your React application. By using this pattern, you can easily add or modify protected routes without duplicating authentication logic throughout your codebase.
Let's look at a basic implementation:
import React, { useContext } from 'react';
import { Navigate, Outlet } from 'react-router-dom';
const ProtectedRoute = ({ }) => {
const token = localStorage.getItem("auth_token");
if ( token == null) {
return <Navigate to="/signin" replace />; // Redirect to login on failure
}
return <Outlet />
};
export default ProtectedRoute;
In this setup, <Outlet />
serves as a placeholder for rendering child routes. It's a powerful alternative to passing children as props.
We can then use this ProtectedRoute component in our router configuration:
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/">
<Route element={<ProtectedRoute />}>
<Route path="home" element={<HomePage />} />
</Route>
<Route path="signin" element={<Signin />} />
<Route path="signup" element={<Signup />} />
<Route path="*" element={<h1>Page not found</h1>} />
</Route>
)
);
Loaders: A New Way to Handle Authentication
For public routes, React Router 6 introduces a powerful new feature called loaders. These functions allow us to perform checks or data fetching before a route is rendered. In our case, we're using loaders to implement a form of "reverse authentication" - ensuring that authenticated users are redirected away from public routes they no longer need to access, such as the sign-in page.
Here's how we're utilizing loaders:
We've created an
isAuthenticated
function that checks for user authentication.This function is used as a loader for our public routes.
If a user is already authenticated, the loader redirects them to an appropriate page (e.g., the dashboard).
Here's an example implementation:
import { redirect } from "react-router-dom";
export const isAuthenticated = async () => {
const token = localStorage.getItem("token");
if (!token) throw redirect("/signin");
return null;
};
// Usage in router
<Route
path="dashboard"
element={<Dashboard />}
loader={async () => await isAuthenticated()}
/>
This approach allows for more declarative and centralized authentication logic.
Putting It All Together
Finally, we use the RouterProvider
component to render our routes:
import React from 'react';
import { RouterProvider } from 'react-router-dom';
import routes from './routes';
import { AuthProvider } from './providers/AuthContext';
const App = () => {
return (
<AuthProvider>
<RouterProvider router={routes} />
</AuthProvider>
);
};
export default App;
The evolution of React Router reflects a deeper understanding of how developers build modern web applications. With improvements in nested routes, protected routes, and the introduction of concepts like loaders, React Router has become more powerful and developer-friendly.
Further article to read on this topic:
https://remix.run/blog/react-router-v6
Wishing you all the best with your coding journey. Happy learning, and follow along for more insightful articles!
Connect with me on LinkedIn: Abhishek Bhatt
Subscribe to my newsletter
Read articles from Abhishek bhatt directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by