You Might Not Need React Router

React Router is the 800-pound gorilla of routing in the React ecosystem. According to the State of JavaScript 2019 survey, 63% of React developers use React Router for client-side routing. It‘s a powerful, feature-rich library that can handle even the most complex routing requirements.

However, not every React app needs the full power of React Router. For applications with simple routing needs, React Router can be overkill. It adds a significant amount of library code to your app bundle (~20kb gzipped) and introduces a steeper learning curve than basic routing. In many cases, a lightweight alternative or even a custom solution may be a better fit.

How React Router Works

Before we dive into the alternatives, let‘s take a quick look at how React Router works under the hood.

At its core, React Router is a state container for the current location. It provides a way to map URL paths to different React components. When the URL changes, React Router updates its internal state and re-renders the appropriate component hierarchy.

Here‘s a simple example of how you might define routes in a React Router application:

import { BrowserRouter as Router, Route } from ‘react-router-dom‘;

function App() {
  return (
    <Router>
      <div>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route path="/contact" component={Contact} />
      </div>
    </Router>
  );
}

In this example, the <Route> components define a mapping between URL paths and React components. When a <Route>‘s path matches the current URL, its component will be rendered.

React Router uses a declarative API for defining routes. You specify your application‘s routes upfront as part of your component hierarchy. This makes it easy to understand at a glance what views map to which URLs.

Under the hood, React Router uses a route matching algorithm to determine which <Route> to render. It compares the current URL against the path of each <Route> in the order they are defined. When it finds a match, it renders that <Route>‘s component and ignores all subsequent <Route>s.

The HTML5 History API

React Router builds on top of the browser‘s HTML5 History API. This API allows JavaScript to manipulate the browser history without triggering a full page reload.

The key methods provided by the History API are:

  • history.pushState(state, title, url) – adds a new entry to the history stack
  • history.replaceState(state, title, url) – modifies the current entry in the history stack
  • history.go(n) – navigates n entries forward or backward in the history stack

These methods allow you to programmatically navigate to new URLs and associate arbitrary state with each entry in the history stack.

Here‘s a simple example of using the History API to navigate to a new URL:

function navigate(url) {
  window.history.pushState(null, null, url);
}

navigate(‘/new-url‘);

Calling navigate(‘/new-url‘) will push a new entry onto the history stack with the URL /new-url. The browser‘s address bar will update to show the new URL, but crucially, the page will not reload. This enables smooth, fast client-side transitions between different application states.

React Router wraps the History API in a higher-level API and provides additional features like route matching and declarative routing. But at its core, it‘s just a state management library built on top of the History API.

Routing Without React Router

So if React Router is just a wrapper around the History API, do you really need it? For many simple use cases, you can get by with using the History API directly or with a lightweight alternative library.

Let‘s look at a few different approaches for routing without React Router.

Approach 1: Use the History API directly

The simplest approach is to use the History API directly to manage navigation state. You can create your own navigate function that uses history.pushState() to update the URL and then manually re-render your application.

Here‘s an example of how you might implement a basic routing solution using the History API:

import React from ‘react‘;
import { render } from ‘react-dom‘;

// Define your routes as an object mapping URL paths to components
const routes = {
  ‘/‘: Home,
  ‘/about‘: About,
  ‘/contact‘: Contact,
};

// Define a navigate function that updates the URL and re-renders the app
function navigate(url) {
  // Update the URL
  window.history.pushState(null, null, url);

  // Re-render the app
  render(<App />, document.getElementById(‘root‘));
}

function Link({ to, children }) {
  // Render links that call navigate when clicked
  return (
    <a
      href={to}
      onClick={e => {
        e.preventDefault();
        navigate(to);
      }}
    >
      {children}
    </a>
  );
}

function App() {
  // Determine the current route by looking at window.location
  const pathname = window.location.pathname;

  // Find the component to render based on the current route
  const Component = routes[pathname] || NotFound;

  // Render the component
  return (
    <>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/contact">Contact</Link>
      </nav>
      <Component />
    </>
  );
}

// Define your route components
function Home() {
  return ;
}

function About() {
  return ;
}

function Contact() {
  return ;
}

function NotFound() {
  return ;
}

render(<App />, document.getElementById(‘root‘));

In this example, we define our routes as a simple object mapping URL paths to React components. We then use the window.location object to determine the current route and render the appropriate component.

Navigation is handled by a custom <Link> component that calls our navigate function with the appropriate URL when clicked. The navigate function updates the URL using history.pushState() and then manually re-renders the entire application.

While this approach is simple, it has some limitations. It doesn‘t support more advanced routing features like nested routes, URL parameters, or lazy loading. It also requires a full re-render of the application on every URL change, which can hurt performance.

Approach 2: Use a lightweight routing library

If you need a bit more functionality than the basic History API provides, you might consider using a lightweight routing library. Some good options are:

  • Navaid – A tiny (570 byte) navigation aid for SPAs
  • Impress – An Express-like router for React
  • curi – A JavaScript router for single-page applications

These libraries typically provide a more declarative API for defining routes and a way to associate route changes with component updates. They aim to give you the core benefits of a routing library with a smaller footprint than React Router.

For example, here‘s how you might define routes using Navaid:

import navaid from ‘navaid‘;

const router = navaid();

router
  .on(‘/‘, () => {
    // Render home component
  })
  .on(‘/about‘, () => {
    // Render about component  
  })
  .on(‘/contact‘, () => {
    // Render contact component
  });

router.listen();

With Navaid, you define your routes using a chainable API. Each route is associated with a handler function that renders the appropriate component for that route. Navaid takes care of updating the URL and calling the correct handler whenever the route changes.

Using a lightweight library like Navaid gives you a bit more structure and declarative syntax than rolling your own solution with the History API. It can be a good middle ground between a full-featured library like React Router and a completely custom solution.

Approach 3: Build a simple custom router

For the simplest routing needs, even a lightweight library may be more than you need. In some cases, a basic custom router can be implemented in just a few lines of code.

Here‘s an example of a bare-bones router using just the window.location object and a switch statement:

import React from ‘react‘;
import { render } from ‘react-dom‘;

function App() {
  // Determine the current route
  const pathname = window.location.pathname;

  // Render the appropriate component based on the route
  switch (pathname) {
    case ‘/‘:
      return <Home />;
    case ‘/about‘:
      return <About />;
    case ‘/contact‘:
      return <Contact />;
    default:
      return <NotFound />;
  }
}

// Define your route components
function Home() {
  return ;
}

function About() {
  return ;
}

function Contact() {
  return ;
}

function NotFound() {
  return ;
}

render(<App />, document.getElementById(‘root‘));

This basic router simply looks at window.location.pathname to determine the current route and renders the appropriate component based on a switch statement.

You can extend this basic implementation with a <Link> component for navigation:

function Link({ to, children }) {
  return (
    <a
      href={to}
      onClick={e => {
        e.preventDefault();
        window.history.pushState(null, null, to);
        render(<App />, document.getElementById(‘root‘));
      }}
    >
      {children}
    </a>
  );
}

The <Link> component renders an <a> tag that updates the URL and re-renders the app when clicked, similar to the previous examples.

This approach is about as simple as it gets for routing. There‘s no external library, no complex configuration, just a few lines of code to map URLs to components.

The main limitation is that it doesn‘t support more advanced routing features like nested routes, route parameters, or asynchronous route loading. It also requires a full re-render on every route change, which can impact performance for larger apps.

Trends and Future Direction

As the React ecosystem continues to evolve, new approaches to routing are emerging that challenge the dominance of React Router.

One notable trend is the rise of file-based routing solutions like Next.js and Gatsby. These frameworks automatically map files in a /pages directory to routes, eliminating the need to manually define a route configuration. This can significantly simplify the routing setup for many applications.

pages/
  index.js       // -> /
  about.js       // -> /about
  contact.js     // -> /contact
  blog/          // -> /blog/*
    index.js     // -> /blog
    [slug].js    // -> /blog/:slug

Another trend is the increasing popularity of hook-based routing libraries like Reach Router and React Location. These libraries aim to provide a simpler, more composable API based on React Hooks.

For example, here‘s how you might define routes with React Location:

import { ReactLocation, useMatch } from ‘react-location‘;

const location = new ReactLocation();

function App() {
  return (
    <Router location={location}>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/contact" element={<Contact />} />
    </Router>
  );
}

React Location‘s useMatch hook can then be used within components to access routing data:

function BlogPost() {
  const { params: { slug } } = useMatch();

  return <Post slug={slug} />;
}

These hook-based libraries offer a fresh take on routing that aligns well with the overall direction of the React ecosystem.

Conclusion

React Router is a powerful tool, but it‘s not the only way to handle routing in a React application. For simpler use cases, you may be better off with a lightweight alternative or even a custom solution.

Before reaching for React Router, consider the following questions:

  1. How complex are my routing requirements? Do I need advanced features like nested routes, route guards, or code splitting?
  2. How large is my application? Will adding a full-featured routing library significantly impact my bundle size?
  3. How much control do I need over the routing behavior? Do I want to handle every aspect of routing myself or defer to a library?

If you have simple routing needs, a small application, or a desire for full control, you may not need React Router. A lightweight library or custom solution may be a better fit.

Conversely, if you have complex routing requirements, a large application, or limited time to spend on routing, React Router is a solid choice. It‘s a well-tested, feature-rich library with strong community support.

Ultimately, the "right" routing solution will depend on your specific application and priorities. But by carefully considering your requirements and exploring the alternatives to React Router, you can make an informed decision that balances functionality, performance, and maintainability.

As the React ecosystem continues to evolve, it‘s worth keeping an eye on emerging trends and new approaches to routing. File-based routing and hook-based libraries are two areas to watch in the coming years. But regardless of what the future holds, the principles of choosing the right tool for the job will always apply. Don‘t be afraid to question established norms and explore alternative solutions – you might just find a better way!

Similar Posts