Suspense

Suspense est une fonctionnalité apparue dans React 16.6 et permettant de gérer plus facilement l'asynchrone dans vos composants react. L'utilisation la plus basique qui est présentée dans la doc de React permet de se passer de react-loadable et de charger des modules dynamiquement sans bibliothèque tierce. Ainsi, le code suivant permet de générer des chunks (fichiers javascript compilé et chargé à la demande) pour chaque module de notre navigation :

import React, { lazy, Suspense } from 'react';  
import { Switch, Route } from 'react-router';

import Loading from './Loading';

const Home = lazy(() => import('./Home');  
const Foo = lazy(() => import('./Foo');

export const MainView = () => (  
  <Switch>
    <Route path="" exact>
      <Suspense fallback={Loading}>
        <Home />
      </Suspense>
    </Route>
    <Route path="/foo">
      <Suspense fallback={Loading}>
        <Foo />
      </Suspense>
    </Route>
  </Switch>
)

La fonction import() est standard et permet, selon la spécification ECMAScript, de charger des modules dynamiquement pendant l'exécution du code. La fonction est mise en pratique grâce à babel et webpack sous la forme de fichiers chunks : des fichiers pré-compilés séparés du bundle principal qui peuvent donc être chargés uniquement quand cela est nécessaire. Cela permet d'alléger le premier chargement de votre application en évitant de charger le code de vues que votre utilisateur n'ira jamais consulter.

Mais on peut aller plus loin.

Pré-chargement de données

Ce qui prend le plus de temps lorsqu'on charge une vue, ce n'est pas le module javascript qui n'est qu'un fichier statique de quelques centaines de Ko pour les plus gros, mis en cache par le navigateur. C'est le chargement des données initiales à faire afficher à notre vue. Prenons un exemple d'une vue de billet de blog. Le composant qui va afficher le billet est très simple : il se contente d'afficher la prop title et la prop content. Allez, on va l'alourdir un peu en lui faisans embarquer le package markdown car notre content sera en markdown. Le module et ses dépendances ne feront pas plus de quelques dizaines de Ko et sera chargé en moins de 40ms. Cependant, une fois le module chargé, le composant va aller regarder dans l'url quel est l'id du billet à charger et à afficher. S'en suit donc une requête xhr vers un backend plus ou moins lent nécessitant d'afficher un loader supplémentaire.
Grâce à Suspense, il est possible de n'avoir qu'un seul et unique loader pour toute la séquence de chargement : composant + données. C'est ce que proposait AngularJS sous la forme des resolvers et Aurelia-router sous la forme de la méthode activate et qui m'a toujours manqué sur React.

L'idée est d'exporter en plus de notre vue une fonction qui retourne une promesse dont la résolution conviendra à lazy, à savoir : un object avec une propriété default de type function.

Le composant Test est chargé dynamiquement, on attend sa résolution dont on extrait une fonction withLazyData préalablement exportée qu'on exécute et dont on retourne le résultat à lazy. withLazyData va faire son travail asynchrone de récupération de données, et retourne une promesse résolvant un objet dont la propriété default est notre composant avec les données passées en props. Ainsi, le composant généré par lazy ne s'affichera que lorsque le module sera chargé et que les données auront été passées au composant.

Hadrien

Hi, I'm a french Javascript Lead Developer, Web Architect from Toulouse, France. I've worked for 12 years for many projects with YUI, AngularJS, Aurelia.io and now React and React native.

Toulouse, France https://hadrien.eu