Back to the future: Navigation API
Tags: javascript, tutorial, webdev
Published: 2022-12-19 18:33:21

In my company's projects, I forbid to migrate our `react-router` to the new `v6`.

**Why?**
Because they removed the ability to block the navigation, for example when a form hasn't been saved and the user click on a link.

There is an opened issue [[V6] [Feature] Getting usePrompt and useBlocker back in the router ](https://github.com/remix-run/react-router/issues/8139), and recently **Ryan florence** (one of the create of **React router** and **Remix**) commented on this and said that they removed the feature because has always had corner case where it will not work. Corner cases that will be fixed thanks to **the new Navigation web API**.

Let's see the innovation that introduces this new API.

> **Note:** Currently, routing libraries like `React Router`, `Vue Router`, `TanStack Router`, `SvelteKit Routing`, ... use the history API. If you want to know more about this API you can read [Let's explore javascript's Location and History API](https://dev.to/romaintrotard/let-s-explore-javascript-s-location-and-history-api-26pc).

---

## New NavigationHistoryEntry

### Entry list

Have you ever wanted to get the list of entry in the history? I have!

Here is the use case:
- You are on a article listing page
- You filter by title (because only want article about the Navigation API)
- Clicking on an article on the list to see the detail
- You are redirected to the detail page
- When you are done
- You want to go back to the listing page thanks to a "_Go back to listing page_" button
- You expect to go back to the listing page with the previous filter on year you made


![Go back to listing page example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e12l4u7dqmreupm8zafj.png)


With the **History API** it's not possible to easily do that, because you only know the number of items in the history entries.

To do the previous scenario, you have to either:
- keep all the url in a global state
- or only store the latest `search` in a global state

Both strategies suck.

Thanks to the new `navigation.entries()` that returns a list of `NavigationHistoryEntry`:


![Navigation entries example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a2dp079yahyyyk2r3u8p.png)

Amazing!
But this is not enough for our use case, because we can have multiple entry with the listing page url in our history.
And the user can be at any entry in the history list if playing with the "backward" / "forward" browser buttons.

For example we could have the following history entry list:

![Problem with entry list](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tjhjs7n6flck2rwgyrqr.png)


So we need to know where we are in the history entries. Fortunately, there is a new property for this. Let's see it.

{% details More information %}

Each `origin` has its own navigation history entries. 

So for example if a user navigates is on a site with the `romaintrotard.com` origin, all entries will be added on the list and will be visible with `navigation.entries()`.

But if the user then goes to `google.com`, if we relaunch `navigation.entries()`, there is only one entry because a brand new list has been created for the `google.com` origin.

Then if the user does some backward navigation and go back to `romaintrotard.com`, the navigation history entries will be the previous one, so there will be more than one entry.

{% enddetails %}


----

### Current entry

Thanks to the `navigation.currentEntry` we can know where we are:

![Current entry example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cl6hjiyk45mwizhd3hvr.png)

And now we can get our previous entry corresponding to the listing page. We just have to combine this value with the `navigation.entries()`:

```js
const { index } = navigation.currentEntry;
const previousEntries = navigation.entries().slice(0, index);
const matchingEntry = previousEntries.findLast((entry) => {
    // We have the `url` in the entry, let's
    // extract the `pathname`
    const url = new URL(entry.url);

    return url.pathname.startsWith("/list");
})
```

----

## New way to to navigate between pages

With this new API, it will not be necessary to use the `history.replaceState` and `history.pushState` anymore but just do:

```js
const { committed, finished } = navigation.navigate(
    "myUrl",
    options,
);
```

Here is a non exhaustive list of the available options:
- `history`: defines if it is `replace` or `push` mode
- `state`: some information to persist in the history entry

You probably wonder "_Why is it better than the `history` API, because for now it seems to do the same things._".
And you are right. 
Let's see 2 differences.

### `committed` and `finished` returned values

It returns an object with two keys that can be useful when working with Single Page App: 
- `committed`: a promise that fulfills when the visible url has changed and the new entry is added in the history
- `finished`: a promise that fulfills when all the interceptor are fulfilled

Thanks to the `finished` promise, you can know if the navigation has been aborted, or if the user is on the right page.

Thanks to that we can display some feedback to the user when changing of page.

```js
<button
    type="submit"
    onClick={async () => {
        setLoading(true);

        try {
            // Send values to the back
            await submit(values);
        } catch (e) {
            showSnackbar({
                message:
                    "Sorry, an error occured " +
                    "when submitting your form.",
                status: "error",
            });
            setLoading(false);
            return;
        }

        try {
            // Then redirect to the listing page
            await navigate("/list", {
                history: push,
                info: "FromCreationPage",
                state: values,
            }).finished;
        } catch (e) {
            showSnackbar({
                message:
                    "Sorry, an error occured " +
                    "when going to the listing page.",
                status: "error",
            });
        } finally {
            setLoading(false);
        }
    }}
>
    Save
</button>
```

### Loading feedback

Another difference with the history navigation is that the browser will display a feedback to the user on its own when the page is changing, like if we were on a Multiple Page Application.


![Browser displays loading feedback](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/301e0kx1nazu9368mf1z.gif)

----

## Navigation through the entry list

We are going quickly on the new way to navigate through the `NavigationHistoryEntry` list.

> **Note:** All this new methods returned the same object than `navigate` with `committed` and `finished` promises.


### Reload of the page

Until now, you can do it thanks to `location.reload()`. Now, the new way will be:

```js
const { committed, finished } = navigation.reload();
```

----

### Go to the previous page

The new way to go the previous navigation history entry is to use:
```js
const { committed, finished } = navigate.back();
```

The previous way to do that is with `history.back()`.

### Go to the next page

You probably already guessed it, you can also go to the next navigation history entry with:
```js
const { committed, finished } = navigation.forward();
```

The previous way to do that is with `history.forward()`.

----

### Go to a more distant entry

Previously to go to another history entry, you will have to know the number of entry to jump. Which is not an easy way to do it, because you don't have a native way to get this information.


So you had to do a mechanism to have this value. For example by maintaining a global state with all the previous url / entry. 

And then use:

```js
history.go(delta);
```

{% details History list using routing library %}


**Note:** If you use a routing library that overrides the native `history` API, you probably have a way to listen all the navigation:
```js
const myLocationHistory = [];

history.listen((newLocation, action) => {
    if (action === "REPLACE") {
        myLocationHistory[myLocationHistory.length - 1] =
            newLocation;
    } else if (action === "PUSH") {
        myLocationHistory.push(newLocation);
    } else if (action === "POP") {
        myLocationHistory.pop();
    }
});
```

{% enddetails %}

With the new **Navigation Web API**, there is a more straightforward way to do it with:

```js
const { committed, finished } =
    navigation.traverseTo(entryKey);
```

The `entryKey` can be deduced thanks to the code in the "Current entry" part. Amazing!

{% details Example of code %}

```js
const { index } = navigation.currentEntry;
const previousEntries = navigation.entries().slice(0, index);
const matchingEntry = previousEntries.findLast((entry) => {
    // We have the `url` in the entry, let's
    // extract the `pathname`
    const url = new URL(entry.url);

    return url.pathname.startsWith("/list");
})

if (matchingEntry) {
    navigation.traverseTo(matchingEntry.key);
}
```

{% enddetails %}

----

## New NavigationEvent

You can subscribe to `navigate` event to be notified of all navigation event.

```js
navigation.addEventListener("navigate", (event) => {
	console.log(
		"The new url will be:",
		event.destination.url,
	);
});
```

> **Note:** The event listener can be asynchronous. In this case, the browser loading feedback will be displayed until it fulfills.

The `NavigateEvent` is fired for the following cases:
- navigation with the `location` API
- navigation with `history` API
- navigation with the new `navigation` API
- browser back and forward buttons

But will not catch:
- reload of the page with the browser button
- change of page if the user changes the url in the browser

For these two cases you will have to add a `beforeunload` event listener:

```js
window.addEventListener("beforeunload", (event) => {
	// Do what you want
});
```

----

### Blocking navigation

One of the interesting things you can do, is **blocking the navigation** thanks to `event.preventDefault()`:

```js
navigation.addEventListener("navigate", (event) => {
	if (!hasUserRight(event.destination.url)) {
                // The user 
		event.preventDefault();
	}
});
```

> **Note:** It's thanks to this feature that we will be able to stop the navigation and prompt a modal when there is unsaved changed on a form.

----

### Simulating SPA


You probably know that, routing libraries prevent the default behavior of links thanks to `preventDefault`. This is the way we can change the url without having a full page reload.

> **Note:** I have an article to know the difference between `stopPropagation` and `preventDefault`: [preventDefault vs stopPropagation](https://dev.to/romaintrotard/preventdefault-vs-stoppropagation-g11).

It's now possible to override this default behavior for every link without `preventDefault`.

You just have to add an **interceptor** to the `NavigateEvent`:

```js
navigation.addEventListener("navigate", (event) => {
    event.intercept({
        handler: () => {
            // Do some stuff, it can be async :)
            // If async, the browser will be in 
            // "loading mode" until it fulfills
        },
    });
});
```

----

## Conclusion

The **Navigation API** brings some new ways to handle navigation and be notified of them that should simplified some tricky part in routing libraries. For example, when wanting to block the navigation when there are unsaved changes on a form.
I think this new API will replace the history one that will die slowly. 
But watch out, you probably shouldn't use it right now because **Firefox** and **Safari** do not support it.

But you can play with **Chrome** and **Edge** :)
If you want to know more about it, you can read [the specification](https://github.com/WICG/navigation-api).

Stay tuned, in a future article I will put all this into practice by implementing a small routing library.

----

Do not hesitate to comment and if you want to see more, you can follow me on [Twitch](https://twitch.tv/rainbowapps ) or go to my [Website](https://www.romaintrotard.com). And here is a little link if [you want to buy me a coffee](https://ko-fi.com/romaintrotard) :coffee: