Routing in React with Universal Router
Tags: react, tutorial
Published: 2022-07-14 12:19:00



A quick showcase of [UniversalRouter](https://www.npmjs.com/package/universal-router/v/9.1.0) (1.6kB zip) as an "easy" alternative to React Router.

{%codesandbox youthful-driscoll-38h13n%}

What is it? The code used for the router is not embedded within React components. It is pure Javascript code that allows executing code on a given path, and then delivers React components to React for rendering. It uses the browser navigation and the [History interface](https://developer.mozilla.org/en-US/docs/Web/API/History).

What is the point of using this? An example: when you navigate to a page, you may want to render data. Instead of using a `useEffect` in the component, you can pre-fetch the data and then pass it as an argument to a stateless React component, all this asynchronously (and no double rendering).
Lastly, the code is pretty stable :)

In this showcase, we don't use redirects, just a "nabvar" component with links that stays on top of each page and renders components as `children`.

## Map of "links"

A navbar is a collection of links. Each link has a `path` and `title` attribute. We define a map of objects that contain these attributes:

```js
export const useLinks = [
  { path: "/", title: "Home" },
  { path: "/users", title: "pre-render" },
  { path: "/vusers", title: "Valtio store" },
  [...]
];
```

Each object of this array will be the arguments of a "link" element.

```js
const Link = ({ path, title, handler }) => 
  <a href={path} onClick={handler}>
    {title}
  </a>
```

The `onClick` handler is defined in the parent component "Navbar". If any extra code needs to be executed for a given path, we can define it in our route array, as seen further down.

## The Navbar

We build the Navbar component by iterating over the map of `<Link />` objects.
The "onClick" handler will simply push the found *pathname* attribute into the [browser history session](https://www.npmjs.com/package/history). The Navbar will render any component as a child.

```js
const NavBar = ({ children }) => {
  function handleNav(e) {
    e.preventDefault();
    history.push({ pathname: e.target.pathname });
  }

  return (
    <>
      {useLinks.map(({ path, title }) => (
        <Link key={title} path={path} title={title} handler={handleNav} />
      ))}
      {children}
    </>
  );
};
```

## The routes

The routing is made by defining a `route`  object which is an array of ` path`  objects. A path object is defined with three reserved keys: `path`, `action` and ` children`. A path object has the form: 

```js
{path: "/component", action: handler, children: []}
```

The optional `children` array will give **nested** routes.

An `action` is simply a function. It will eventually return a React component (in our case as we have no redirects). Every `action` can be made **async**, in particular, we can make **dynamic imports**.

For example, suppose we want to navigate to a page which displays data retrieved from an api. The action can be:

```js
async function PreFetch() {
  const users = await fetchComments(8);
  const { default: Users } = await import("../utils/users");
  return <Users data={users} />;
}
```

and our path object is:

```js
{ path: "/users", action: async () => PreFetch() }
```

It admits an object `context` that can be used by the path objects. The `action` accepts the `context` object from the routes as an attribute. We can use this to pass a data store for example (we showcased a [Valtio](https://github.com/pmndrs/valtio) data store here) so that we don't need to spread the store through the code. Just inject it into the component through the routes. Easy!
The context object also captures the "params" if needed.

An example of a route array that **UniversalRouter** will transverse:

```js
const routes = [
  {
    // wrapping the routes with the Navbar and render every component as a child
    path: "",
    action: async ({ next }) => {
      const component = await next();
      const { default: NavBar} = await import('./NavBar')
      return component && <NavBar>{component}</NavBar>
    },
    children: [
      {
        path: "/",
        action: async () =>
          import(".Home").then(({ Home }) => <Home />)
      },
      {
        path: "/users",
        action: async () => PreFetch()
      },
      {
        path: "/vusers",
        async action({ vStore }) {
          await vStore.getUsers(2);
          const { default: Users } = await import("../utils/users");
          return <Users data={vStore.users} />;
        }
      },

      {
        path: "(.*)",
        action: () => <img scr="404.webp" ...</h1>
      }
    ]
  }
];
```


```js
const Users = ({ data }) => (
    <>{data && data.map((user) => <User key={..}... />)}</>
  );
```


## Route parsing and rendering

- We initiate the [browser history session](https://github.com/remix-run/history/blob/3e9dab413f4eda8d6bce565388c5ddb7aeff9f7e/docs/installation.md) as:

```js
import { createBrowserHistory } from "history";
export default createBrowserHistory();
``` 

- We initiate our `router` object and can pass in some `context` whenever an `action` might need it. For example, we use a data store (called `vStore` here) managed by Valtio: 


```js
// example of Valtio store
import { proxy, useSnapshot } from "valtio";
import { fetchUsers } from "./fetchUsers";
export { useSnapshot };

export const vStore = proxy({
  users: null,
  async getUsers(id) {
    vStore.users = await fetchUsers(id);
  },
});
```

We can pass it to the ` context` key in the constructor and any path object `action` method can use this store with `action(context) {...}` whenever needed.

``` js
const router = new UniversalRouter(routes, {context: {vStore}});
``` 

## How does this work?

The `history` listens to paths change and triggers a `renderRoute` function. *UniversalRouter* transverses the "routes" array used in the constructor to find a match with the path. It then executes the action which will return a React component (in our case). It then calls the ` React.render` function on the returned function.  

```js
import { createRoot } from "react-dom/client";
import React from "react";

import UniversalRouter from "universal-router";
import history from "./router/history";

import routes from "./router/routes";

import { vStore } from "./valtio/vStore";


const context = { vStore };

const router = new UniversalRouter(routes, { context });

const root = createRoot(document.getElementById("root"));

async function renderRoute(location) {
  try {
    // "history" returns a path, and "router" finds a match in the routes array
    const page = await router.resolve({
      pathname: location.pathname
    });

    return root.render(<>{page}</>);
  } catch (err) {
    console.log(err);
    return root.render(<p>Wrong!</p>);
  }
}

history.listen(({ location }) => renderRoute(location));
history.push("/");
```

## A word about redirection

Just return a path that returns an object `{redirect: "/path_you_want"}`. With the middleware approach and our Navbar, we modify the path:
```js
path: "",
    action: async ({ next }) => {
      const component = await next();
      const { default: NavBar} = await import('./NavBar')
      if (component.redirect) return component
      return component && <NavBar>{component}</NavBar>
    },
```

Since "component" will contain the key "redirect", it remains to let the `resolve` function return the path in the "main.js". This works thanks to the key "redirect":

```js
const page = await router.resolve({
  pathname: location.pathname
});
if (page.redirect) {
  return history.push(page.redirect);
}
return root.render(<>{page}</>);
```