📐 Construire une app en JS
26. Action et Loader

Action et Loader

Notre page principale affiche la liste des logements.

Alors on a furieusement envie de cliquer sur un bouton réserver pour appeler notre commande book. Malheureusement, il ne se passe rien.

Mais qu'attendait-on exactement ?

Reprenons notre test :

const during = await app.dependencies.bookings.getAvailableAccommodations(
    Stay.parse({ from: "2024-06-01", to: "2024-06-03" }).value
);
expect(during.some(accommodation => accommodation.id === "accommodation-1")).toBe(false);

Après avoir réalisé une réservation d'un logement sur une période, le logement ne figure plus dans la liste des logements disponibles à cette période. Donc le logement réservé devrait "disparaître" de la liste.

Pourquoi n'est-ce pas ce qu'il se passe ?

Boucler la boucle

Rappelons-nous notre schéma de principe de l'animation de notre page :

   LOADER --- alimente ---> RENDER
      |                        |
 re-déclenche              déclenche
      ^                        |
      |                        |
      ----------- ACTION <------

Après une action, il nous faut actualiser la vue en invoquant à nouveau le loader.

En l'état, notre code n'implémente pas ce lien : il nous faut boucler la boucle.

Pour cela, commençons par modifier notre hook pour exporter un moyen de recharger les données, au travers d'une fonction refresh :

export function useAccommodations() {
  // Nos 2 états sur la page
  const [loading, setLoading] = useState(false);
  const [accommodations, setAccommodations] = useState([]);
  // Une fonction qui orchestre les changements d'état
  const loadAccommodations = async () => {
    setLoading(true);
    setAccommodations(await loader());
    setLoading(false);
  };
  // Un effet
  useEffect(() => {
    loadAccommodations();
  }, []);
  return { accommodations, loading, refresh: loadAccommodations };
}

Et utilisons cette fonction pour rafraîchir la page à condition que l'action réussisse :

function HomePage() {
  const { accommodations, loading, refresh } = useAccommodations();
  const [error, setError] = useState(null);
 
  const onBook = async (accommodationId) => {
    const { status, error } = await action(accommodationId);
    if (status === "ok") {
      setError(null);
      await refresh();
    } else {
      setError(error);
    }
  };
 
  return (
    <Layout loading={loading} error={error}>
      {accommodations.map((accommodation) => (
        <Accommodation
          key={accommodation.id}
          accommodation={accommodation}
          onBook={() => onBook(accommodation.id)}
        />
      ))}
    </Layout>
  );
}

Notez ce que nous faisons du cas d'erreur. La tentation est grande d'écrire status === "ok" ? refresh() : {} et de passer à autre chose — mais un échec silencieux, pour l'utilisateur, c'est un bouton qui ne marche pas.

Or nos commandes ne lèvent jamais d'exception : elles retournent un contexte porteur de son erreur. Nous avons donc, en une ligne, de quoi dire à l'utilisateur ce qui s'est passé. Ce serait dommage de le jeter.

C'est le bénéfice différé de la décision prise au chapitre 14 : préférer le résultat à l'exception. Deux mois plus tard, dans une couche complètement différente, l'information est encore là.

La boucle est bouclée : loader, render, action. Notre première page fonctionne.

Il ne reste qu'un détail, en haut de l'écran : ce bandeau de recherche qui ne sert encore à rien.