Navigation API: la navigation côté client la plus moderne
Tags: javascript, webdev, news, productivity
Published: 2022-08-18 07:49:14

## Introduction
Les SPAs, **S**ingle **P**age **A**pplication, sont caractérisées par une fonctionnalité essentielle: la possibilité de pouvoir changer son contenu dynamiquement en fonction des intéractions de l'utilisateur au lieu de charger des pages entières générées par un serveur.

Cette fonctionnalité a été rendue possible principalement par  **l'History API** et par des workarounds réalisés avec la partie hash (/#hash) de l'url d'un site.

Celle-ci a été développée bien avant que les SPAs soient la norme applicative du Web, et aujourd'hui une approche totalement nouvelle a dû être créée pour répondre aux nouvelles exigences des applications.

Ainsi est née la **Navigation API**. Cette API a totalement été inventée, son but n'est pas simplement de corriger certains aspects de l'History API (ex: Scroll Restauration) mais de répondre à de nouvelles demandes de performance et de centralisation.

Cet article a pour objectif de vous présenter cette nouvelle API.

**Attention** cette API est disponible uniquement à partir de la version chrome 102 et reste encore en beta test.

## A quoi ressemble cette nouvelle API ?


```javascript
navigation.addEventListener('navigate', navigationEvent => {
  navigationEvent.transitionWhile(async() => {
    // Make your own login here
  });
});
```

Beaucoup d'informations en si peu de code.

**navigation** est un objet global qui vous permet d'écouter l'évènement **navigate** d'une application.

Cet évènement est totalement centralisé. Il sera automatiquement déclenché par des actions provoquées par l'utilisateur (comme soumettre un formulaire, cliquer sur un lien, utiliser le système de backward/forward du navigateur) ou de manière programmatique (par le code de votre site).
La plupart du temps, cet évènement vous permet de modifier le comportement par défaut de votre navigateur.
Dans le cas d'une SPA, il s'agit simplement de rester sur la page courante et de remplacer son contenu.

Deux fonctions vous permettent d'intercepter la navigation du navigateur:
- transitionWhile
- preventDefault

La fonction **transitionWhile** prend en paramètre une promesse généralement générée par une fonction asynchrone. En appelant la fonction **transitionWhile** le navigateur comprend que votre code va configurer le prochain état de votre site.

La fonction preventDefault permet d'annuler la navigation dans son intégralité.

Ces deux méthodes sont autorisées dans la plupart des cas. Cependant quelques petites restrictions existent.

Il est impossible de gérer la navigation via la méthode **transitionWhile** si votre navigation n'est pas intra-site. C'est à dire que vous naviguez vers un autre domaine que le site actuel.

Il est impossible d'annuler une navigation via la méthode **preventDefault** si l'utilisateur utilise les boutons de backward/forward du navigateur.

## Pourquoi inventer une nouvelle API ?

L'évènement **naviguer** permet de centraliser la navigation d'une application. Cette centralisation était extrêmement compliquée avec les précédentes APIs, et il fallait principalement "tricker" pour réaliser ce comportement.
Ci-dessous la réalisation du routing d'une SPA avec l'History API:

```javascript
function constructNextPage(event) {
  event.preventDefault();
  window.history.pushState(null, '', event.target.href);
  constructDOM();
}

const links = [...document.querySelectorAll('a[href]')];
links.forEach(link => link.addEventListener('click', constructNextPage));
```

Cette solution fonctionne plutôt bien. Cependant ici on ne centralise uniquement les navigations sur les liens. Il existe bien d'autres façons de naviguer et il deviendra compliqué de toutes les centraliser sans que le code base en soit grandement détérioré.

La Navigation API a pour objectif de répondre à cette problématique.

## Que se passe t'il lors d'une transition ?

Quand la fonction **transitionWhile** est exécutée à partir du "listener" navigate, le navigateur est informé qu'il doit préparer la page pour un nouvel état ou une modification d'état. Le temps de réalisation de cette navigation est variable et est défini par la promesse passée en paramètre de cette fonction.

Il y a de nombreux avantages à désigner cette API de cette manière:
- l'accessibilité
- catcher les évèvements de succès et d'échec de navigation
- catcher l'évènement d'annulation d'une navigation

Lorsque la promesse est résolue, que ce soit en succès ou en error, un évènement est déclenché.

Si la promesse est résolue avec succès, l'évènement **navigatesuccess** est émis. Cet évènement permet de réaliser la logique de fin de navigation.

```javascript
navigation.addEventListener('navigatesuccess', event => {
  showLoading = false;
});
```

A l'inverse, si la promesse est résolue avec une erreur, l'évènement **navigateerror** est émis. Cet évènement permet de réaliser la logique d'erreur lorsque qu'une navigation échoue.

```javascript
navigation.addEventListener('navigateerror', event => {
  showLoading = false;
  alert('Ohhh no an error occured');
});
```

L'évènement **navigateerror** reçoit en paramètre un évènement de type **ErrorEvent** qui permet de gérer l'erreur avec granularité.


Le fait de passer une promesse à la fonction **transitionWhile** rend la navigation asynchrone, ce qui laisse par exemple la possibilité de réaliser une seconde navigation lorsque la première est en cours.
Dans ce genre de situation, il est important d'annuler le traitement asynchrone de la précédente navigation.

L'évènement passé en paramètre de l'évènement **navigate** comporte une propriété **signal** de type **AbortSignal**. Pour résumer, cette propriété est un objet qui permet de stopper certains traitements asysnchrones comme les requêtes HTTP.

```javascript
navigation.addEventListener('navigate', navigationEvent => {
  navigationEvent.transitionWhile(async() => {
    const todos = await (await fetch(url, {
      signal: navigateEvent.signal})).json();
    // make some work with todos
});
```

Pour plus d'informations sur "l'Abortable fetch" c'est par [ici](https://developer.chrome.com/blog/abortable-fetch/)

## Historique, state et navigation programmatique


### Historique

Récupérer l'entrée courante de son historique de navigation est très simple.

```javascript
const { key } = navigation.currentEntry
```

**currentEntry** est un objet possédant la propriété **key**. Cette propriété est très importante. La valeur de cette propriété de type string est unique et réprésente l'entrée courante de l'historique de navigation et son "slot". Cette clé ne change pas même si l'URL ou l'état change puisque le slot reste inchangé.

Au contraire, si l'utilisateur utilise le bouton précédent du navigateur et ouvre une nouvelle fois la page, la clé change et un nouveau slot est créé.

Cette propriété est importante pour les développeurs, elle permet de faire naviguer l'utilisateur facilement d'un état à un autre

```javascript
const { key } = navigation.entry;
const homeButton = document.querySelector("button[name=home]");

homeButton.onclick = () => navigation.transitionTo(key)

await navigation.navigate('/todos').finished
```

Ainsi, si l'utisateur se trouve dans l'état todos, et décide de cliquer sur le bouton home, celui-ci sera correctement redirigé.

Récupérer l'historique de navigation se fait à l'aide de la propriété currentEntries de l'objet **navigation**.


### State

La **Navigation API** offre aux développeurs une notion d'état pour une entrée courante. Cet état est stocké de manière persistente sur l'entrée courante de la navigation mais n'est pas directement exposé à l'utilisateur. Cette fonctionnalité est similaire à la fonction **history.state** de l'**History API** mais reste grandement améliorée par rapport à celle-ci.

Pour récupérer l'état d'une entrée courante, l'object **navigation.currentEntry** expose une fonction **getState**. Cette fonction renvoie une copie de votre état, ainsi les mutations apportées à votre état n'impactent pas l'état persisté sur l'entrée courante.

Pour modifier l'état persisté, une fonction updateCurrentEntry est mise à disposition à partir de l'objet global **navigation**

```javascript
navigation.updateCurrentEntry({ state: { count: 0 } });
const { getState } = navigation.currentEntry;
const count = getState().count;
count++; // count = 1

console.log(getState()); // { count: 0 }

navigation.updateCurrentEntry({ state: { count } });
console.log(getState()); // { count: 1 }
```


### Naviguer de manière programmatique

Naviguer de manière programmatique signifie naviguer par une méthode appelée par le code.

Pour naviguer de manière programmatique, l'objet global **navigation** expose une fonction navigate prenant en premier paramètre une URL de navigation et en second paramètre un objet de configuration.

L'objet de configuration possède trois propriétés:
- state: état de votre nouvelle entrée de navigation
- history: pouvant être setté à 'replace' pour remplacer l'enrée courante dans l'historique
- info: objet pouvant être récupéré grâce à navigateEvent.info

La fonction navigate remplace et améliore les méthodes **location.assign()**, **pushState** ou encore **replaceState()**

```javascript
navigation.navigate('/todos', { state: { count: 0 } });
```

Cette fonction retourne également un objet qui contient deux promesses:
- committed: URL de navigation a changé et une nouvelle entrée dans l'historique de navigation est disponible
- finished: toutes les promesses de **transitionWhile** ont été résolues (succès ou erreur).

Il existe bien d'autres méthodes pour naviguer:
- traverseTo
- back
- forward
- reload

et ces méthodes retournent toutes le même objet.

```javascript
await navigation.navigate('/todos', { state: { count: 0 } }).finished
```

## Conclusion

Cette API est relativement récente, et est disponible à partir de la version Chrome 102, sans activer un flag.

Elle permet de centraliser notre navigation de manière efficace et offre à travers les évènements et les promesses la possibilité à l'utilisateur de customiser sa navigation de manière granulaire.

Malgré sa récente sortie, Cette API promet déjà de grandes choses, alors n'hésitez pas à la tester et faire remonter vos feedbacks.