react-router v6 demystified (part 2)
Tags: react, javascript, tutorial
Published: 2021-09-01 13:09:12

In my [previous article](https://dev.to/romaintrotard/react-router-v6-demystified-part-1-mhe), we have seen what are the new APIs of `react-router` v6. We also have listed what we expect to develop.
In this article, we won't implement the nested `Route` and `Routes`, but don't be afraid it will be done in a next article.

> **Note:** My implementation will not be exactly the same as `react-router` but it will help you to understand how it works and to explore the `react-router` repository on your own.

The goal is to be able to implement something like this:

```jsx
function App() {
  return (
    <Router>
      <Routes>
        <Route path="hobby/" element={<HobbyListPage />} />
        <Route
          path="hobby/:name"
          element={<HobbyDetailPage />}
        />
        <Route path="about" element={<AboutPage />} />
        <Route path="/" element={<HomePage />} />
      </Routes>
    </Router>
  );
}
```

With a set of utility hooks:

```javascript
// To get the current location pathanme, query params and anchor
function useLocation();

// To get the path variables
function useParams();

// To push or replace a new url
// Or to go forward and backward
function useNavigate();
```

Let's start with the `Router` component

----

## `Router` component

### Implementation

This component is the main one. It will provide the `location` and methods to change the url, to components below it (in the tree).

`react-router` provides two router `BrowserHistory` (using the browser's history) and `MemoryHistory` (the history will be stored in memory).

In this article, we will only develop a `BrowserHistory`. 

The location and navigation methods will be stored in a **React context**. 
So let's create it and code the provider:

```javascript
import React from 'react';

const LocationContext = React.createContext();

export default function Router({ children }) {
  return (
    <LocationContext.Provider
      value={{
        // The current location
        location: window.location,
        navigator: {
          // Change url and push entry in the history
          push(to) {
            window.history.pushState(null, null, to);
          },
          // Change url and replace the last entry in the history
          replace(to) {
            window.history.replaceState(null, null, to);
          },
          // Go back to the previous entry in the history
          back() {
            window.history.go(-1);
          },
          // Go forward to the next entry in the history
          forward() {
            window.history.go(1);
          },
          // If we want to go forward or 
          // backward from more than 1 step
          go(step) {
            window.history.go(step);
          }
        }
      }}
    >
      {children}
    </LocationContext.Provider>
  );
}
```

If you try to use these methods to change the url, you will see that it doesn't work.
If you try to play with this code and watch logs, you will see that the component does not render so any component that uses the `location` will not be informed of the new url.
The solution is to store the location in a state and change it when we navigate through the pages.
But we can't just push the `window.location` in this state, because in reality the reference of `window.location` does not change the reference of the object but the object is mutated. So if we do this, it will just do _nothing_.
So we are going to build our own object, and put the values of `pathname`, `search` and `hash`.

Here is the function to create this new `location` object:

```javascript
function getLocation() {
  const { pathname, hash, search } = window.location;

  // We recreate our own object 
  // because window.location is mutated
  return {
    pathname,
    hash,
    search,
  };
}
```

The creation of the state is:

```javascript
const [location, setLocation] = useState(getLocation());
```

Then we just have to change the state when we navigate, for example when we push:

```javascript
push(to) {
   window.history.pushState(null, null, to);
   setLocation(getLocation());
}
```

We could do the same for the methods which navigate in the history entries. But it will not work when we go back or forward with the browser buttons.
Fortunately, there is an event that can be listened for this use case. This event `popstate` is fired when the user navigates into the session history:

```javascript
useEffect(() => {
  const refreshLocation = () => setLocation(getLocation());

  window.addEventListener("popstate", refreshLocation);

  return () =>
    window.removeEventListener("popstate", refreshLocation);
}, []);
```

Finally we got the following for our `Router`:

```javascript
import React, {
  useContext,
  useEffect,
  useMemo,
  useState,
} from "react";

const LocationContext = React.createContext();

function getLocation() {
  const { pathname, hash, search } = window.location;

  // We recreate our own object
  // because window.location is mutated
  return {
    pathname,
    hash,
    search,
  };
}

export default function Router({ children }) {
  const [location, setLocation] = useState(getLocation());

  useEffect(() => {
    const refreshLocation = () => {
      setLocation(getLocation());
    };

    // Refresh the location, for example when we go back
    // to the previous page
    // Even from the browser's button
    window.addEventListener("popstate", refreshLocation);

    return () =>
      window.removeEventListener(
        "popstate",
        refreshLocation
      );
  }, []);

  const navigator = useMemo(
    () => ({
      push(to) {
        window.history.pushState(null, null, to);
        setLocation(getLocation());
      },
      replace(to) {
        window.history.replaceState(null, null, to);
        setLocation(getLocation());
      },
      back() {
        window.history.go(-1);
      },
      forward() {
        window.history.go(1);
      },
      go(step) {
        window.history.go(step);
      },
    }),
    []
  );

  return (
    <LocationContext.Provider
      value={{
        location,
        navigator,
      }}
    >
      {children}
    </LocationContext.Provider>
  );
}
```

> **Note:** I memoized the `navigator` object because we do not want to recreate it each time the location change. But did not memoized the Provider's value because it will be at the top of the React tree and should re-render uniquely when the location changes.

----

### Utility hooks

Now we can implement, some simple hooks which will use this `LocationContext`. We are going to develop:
- `useLocation`: to get the location
- `useNavigator`: to get the navigator part

The implementations are the following ones:

#### `useLocation`

```javascript
function useLocation() {
  return useContext(LocationContext).location;
}
```

#### `useNavigator`

```javascript
function useNavigator() {
  return useContext(LocationContext).navigator;
}
```

----

## `Route` component

It's time to continue our implementation with the `Route` component. The API is simple, it takes:
- the `element` to display
- the `path` for which this route will be displayed

And the implementation is quite simple:

```javascript
function Route({ element, path }) {
  return element;
}
```

As you can see the `path` prop is not used in this component, but by the `Routes` component which decides if this `Route` should be displayed or not.
And this our next part.

----

## `Routes` component

As I said previously, the `Routes` component decides which `Route` to display in function of the location.

### Reminder

Because I don't want this article to be too long and difficult. In this part, we are just going to do routing with no nested `Route` and `Routes`.

But don't be afraid, in an other article I will code all the features wanted.

### Implementation

Now that we know the scope of this article, let's go put our hands in some code.
We know that a `Routes` takes all the possible `Route` as children. From this children, we can loop through this children to extract the `path` of each `Route` from its props to build a simple array of objects, which easier to process than a React element.

So we want to make a function `buildRouteElementsFromChildren` that will return an Array of:

```typescript
type RouteElement = {
  path: string,
  element: ReactNode,
  children: RouteElement[],
}
```

The code of this function is:

```javascript
function buildRouteElementsFromChildren(children) {
  const routeElements = [];

  // We loop on children elements to extract the `path`
  // And make a simple array of { elenent, path }
  React.Children.forEach(children, (routeElement) => {
    // Not a valid React element, let's go next
    if (!React.isValidElement(routeElement)) {
      return;
    }

    const route = {
      // We need to keep the route to maybe display it later
      element: routeElement,
      // Let's get the path from the route props
      // If there is no path, we consider it's "/"
      path: routeElement.props.path || "/",
    };

    routeElements.push(route);
  });

  return routeElements;
}
```

If we take the following `Routes` example:

```javascript
<Routes>
  <Route path="hobby/:name" element={<HobbyDetailPage />} />
  <Route path="hobby" element={<HobbyListPage />} />
  <Route path="about" element={<AboutPage />} />
  <Route path="/" element={<HomePage />} />
</Routes>;
```

Will be transformed into:

```javascript
[
  {
    path: "hobby/:name",
    element: <HobbyDetailPage />,
  },
  {
    path: "hobby",
    element: <HobbyListPage />,
  },
  {
    path: "about",
    element: <AboutPage />,
  },
  {
    path: "/",
    element: <HomePage />,
  },
];
```


Ok, now that we have a simple object, we need to find the first matching `Route` from this object.

We already now all the possible paths. And thanks to the `useLocation`, we know the current `pathname`.

Before doing some code. Let's think about it. 

Unfortunately, we can't just compare the current `pathname` to the Route ones because we have path variables.

Yeah, I guess you already know that we are going to use Regexp :/

For example, if we are at the location `/hobby/knitting/` named `currentPathname`, we want the following path to match:
- `hobby/:name`
- `/hobby/:name`
- `/hobby/:name/`
- `hobby/:name/`

For the leading slash we are going to put a slash before the path, and replace all double slash by one:

```javascript
`/${path}`.replace(/\/\/+/g, "/");
```

For the trailing slash, we are to put an optional trailing slash in the regex:

```javascript
new RegExp(`^${regexpPath}\\/?$`);
```

Now the question is, what is the value of `regexpPath`. The regex has two objectives:
- get the path variable name (after the `:`), here it is `name`
- get the value associated to it, here it is `knitting`


```javascript
// We replace `:pathVariableName` by `(\\w+)`
// A the same time we get the value `pathVariableName`
// Then we will be able to get `knitting` for
// our example
const regexpPath = routePath.replace(
  /:(\w+)/g,
  (_, value) => {
    pathParams.push(value);

    return "(\\w+)";
  }
);
```

Now, that we have seen the complexity, let's make some code:

```javascript
// This is the entry point of the process
function findFirstMatchingRoute(routes, currentPathname) {
  for (const route of routes) {
    const result = matchRoute(route, currentPathname);

    // If we have values, this is the one
    if (result) {
      return result;
    }
  }
  return null;
}

function matchRoute(route, currentPathname) {
  const { path: routePath } = route;

  const pathParams = [];
  // We transform all path variable by regexp to get
  // the corresponding values from the currentPathname
  const regexpPath = routePath.replace(
    /:(\w+)/g,
    (_, value) => {
      pathParams.push(value);

      return "(\\w+)";
    }
  );
  // Maybe the location end by "/" let's include it
  const matcher = new RegExp(`^${regexpPath}\\/?$`);

  const matches = currentPathname.match(matcher);

  // The route doesn't match
  // Let's end this
  if (!matches) {
    return null;
  }

  // First value is the corresponding value,
  // ie: currentPathname
  const matchValues = matches.slice(1);

  return pathParams.reduce(
    (acc, paramName, index) => {
      acc.params[paramName] = matchValues[index];
      return acc;
    },
    {
      params: [],
      element: route.element,
      // We want the real path
      // and not the one with path variables (ex :name)
      path: matches[0],
    }
  );
}
```

Now that we can get the matching route. We are going to render the `Route` and use a React context name `ReuteContext` to put the `params`.

The `Routes` component is:

```javascript
const RouteContext = React.createContext({
  params: {},
  path: "",
});

function Routes({ children }) {
  // Get the current pathname
  const { pathname: currentPathname } = useLocation();
  // Construct an Array of object corresponding to 
  // available Route elements
  const routeElements =
    buildRouteElementsFromChildren(children);

  // We want to normalize the pahts
  // They need to start by a "/""
  normalizePathOfRouteElements(routeElements);

  // A Routes component can only have one matching Route
  const matchingRoute = findFirstMatchingRoute(
    routeElements,
    currentPathname
  );

  // No matching, let's show nothing
  if (!matchingRoute) {
    return null;
  }

  const { params, element, path } = matchingRoute;

  return (
    <RouteContext.Provider
      value={{
        params,
        path,
      }}
    >
      {element}
    </RouteContext.Provider>
  );
}
```  

> **Note:** You may have noticed the use of a function as second parameter to the `replace` method. Very useful to get the name of the path variable.

And now we need our hook to get the params:

```javascript
const useParams = () => useContext(RouteContext).params;
```

----

## Navigation

Thanks to the `useNavigator` hook, we can access to methods to navigate between page.
But the development experience is not necessarily the best. For example:
- Currently, the path is `/hobby`
- I push, `knitting`
- I would like the new path to be `/hobby/knitting`

> **Note:** Using the method `push` of `useNavigator`, will redirect us to `/knitting` :(

And:
- Currently, the path is `/hobby/knitting`
- I push, `/about`
- I would like the new path to be `/about`

> **Note:** Currently it works, with the method `push` of `useNavigator`.

So, to meet these two needs we are going to develop a hook `useResolvePath` which returns us the right path, a hook `useNavigate` and a component `Link` to navigate where we want easily.

> **Note:** For the navigation we are just going to work with String as path. But in reality, with `react-router` you can pass object with the following structure:

```typescript
// For none typescript developers
// The `?` means it's optional
type To = {
  pathname?: string;
  search?: string;
  hash?: string;
} | string;
```

And in the code we should transform `to` as object to string and vice versa, but I repeat myself I'm just gonna work with string in this article for simplicity.


----

### useResolvePath

To resume the strategy if the path to resolve is starting with a `/` then it's an absolute path otherwise a relative path to actual one.

We can get the actual path, thanks to `useRouteContext`.

Let's implement this:

```javascript
// Concat the prefix with the suffix
// Then we normalize to remove potential duplicated slash
function resolvePathname(prefixPath, suffixPath) {
  const path = prefixPath + "/" + suffixPath;

  return normalizePath(path);
}

// We export the utility method
// because we need it for other use cases
export function resolvePath(to, currentPathname) {
  // If the to path starts with "/"
  // then it's an absolute path
  // otherwise a relative path
  return resolvePathname(
    to.startsWith("/") ? "/" : currentPathname,
    to
  );
}

export default function useResolvePath(to) {
  const { path: currentPathname } = useRouteContext();

  return resolvePath(to, currentPathname);
}
```

Then we can develop our `useNavigate` hook and `Link` component thanks to that :)

----

### `useNavigate`

We are going to start with the hook to use it in the component.

This hook will return a callback with the parameters:
- First parameter: `to` which is a string (the url to navigate to) or a number if we want to go backward or forward.
- Second parameter: an object of options. For the article the only option will be `replace` if the user just want to replace the url (`push` by default).

Let's make some code:

```javascript
function useNavigate() {
  const navigator = useNavigator();
  // We want to know the current path
  const { path: currentPath } = useRouteContext();

  // By default it will push into the history
  // But we can chose to replace by passing `replace` option
  // You can pass a number as `to` to go `forward` or `backward`
  return useCallback(
    (to, { replace = false } = {}) => {
      // If to is a number
      // we want to navigate in the history
      if (typeof to === "number") {
        navigator.go(to);
      } else {
        // We want to get the "real" path
        // As a reminder if
        // to starts with / then it's an absolute path
        // otherwise a relative path in relation to currentPath
        const resolvedPath = resolvePath(to, currentPath);
        (replace ? navigator.push : navigator.push)(
          resolvedPath
        );
      }
    },
    [navigator, currentPath]
  );
}
```

### `Link`

We want to be able to open a new tab from our element, and to have the same behavior than a `a` tag. So let's use a `a` with a `href` property.

But if we just do that, the browser will load the page and refetch assets (css, js, ... files). So we need to prevent this default behavior, we are going to put an `onClick` method and `preventDefault` the event.

```jsx
function Link({ to, children, replace = false }) {
  const navigate = useNavigate();
  // We want to get the href path
  // to put it on the href attribtue of a tag
  // In the real inplementation there is a dedicated hook
  // that use the `useResolvePath` hook
  // and transform the result into string
  // (because potentially an object but not in this article)
  const hrefPath = useResolvePath(to);

  // We put the href to be able to open in a new tab
  return (
    <a
      href={hrefPath}
      onClick={(event) => {
        // We want do not browser to "reload" the page
        event.preventDefault();
        // Let's navigate to `to` path
        navigate(to, { replace });
      }}
    >
      {children}
    </a>
  );
}
``` 

And here we go, we can navigate to new pages.

> **Note:** I also developed a `NavLink` which is useful for navigation item, when you want to display which item is active. You can see the code on the codesandbox below. 

----

## Playground

Here is a little code sandbox of this second part of `react-router` implementation:

{% codesandbox react-router-implem-part-2-wzeog %}


## Conclusion

In this article, we have coded the base to make a `react-router` like library. The main goal is to understand how works the main routing library for `React`, in its next version 6.

To resume what we have learned and done in this second article about `react-router` v6:
- The `Router` provides the location and methods to navigate through pages.
- The `Route` corresponding to a specific page / path
- The `Routes` component determines the `Route` to display, and provides the current pathname of the `Route` and the `params`.

Let's meet in my next article which will implement nested `Route` and `Routes`, and also bonus hooks.

If you want to see more about `react-router` v6 which is in beta yet, let's go see the [migration guide from v5](https://github.com/remix-run/react-router/blob/dev/docs/advanced-guides/migrating-5-to-6.md#upgrade-all-switch-elements-to-routes).

---- 

Want to see more ? Follow me on [Twitter](https://twitter.com/romain_trotard ) or go to my [Website](https://www.romaintrotard.com). :panda_face: