Liste des logements disponibles
Par quoi commencer ?
Notre application impose une authentification. Alors on aurait envie de commencer par un écran de login. Mais pour se logger, il faut s'inscrire. Alors peut-être faut-il d'abord faire l'écran d'inscription.
Rien de tout ça ne mérite la première place dans la liste des choses à faire.
Ce qui importe, c'est de présenter une liste de logements, et de pouvoir les réserver.
C'est ce qui fait la valeur de notre application.
Première page
La première page consiste donc à afficher une liste de logements. Pour chacun d'entre eux, on a un bouton "réserver" qui invoque notre commande de réservation.
Rappelons-nous la décomposition de notre page en 3 fonctions :
- loader : obtenir la liste des logements disponibles à la location
- render : afficher la liste des logements
- action : réserver un logement
Ajoutons ces 3 éléments dans notre fichier packages/webapp/src/App.jsx :
import { App, book, login, testDependencies, Stay } from "@booking/core";
const app = new App(testDependencies());
// Les dates du séjour recherché. Elles viendront du bandeau de recherche plus tard.
const searched = Stay.parse({ from: "2024-06-02", to: "2024-06-04" }).value;
async function loader() {
return await app.dependencies.bookings.getAvailableAccommodations(searched);
}
async function action(accommodationId) {
const session = await app.run([
login({ email: "faketenant@mail.com", password: "secret" }),
book({
accommodationId,
adults: 2, children: 3,
from: "2024-06-02",
to: "2024-06-04",
}),
]);
return session.error ? { status: "error", error: session.error } : { status: "ok" };
}
async function render(accommodations) {
return (
<Layout>
{accommodations.map((accommodation) => (
<Accommodation
key={accommodation.id}
accommodation={accommodation}
onBook={() => action(accommodation.id)}
/>
))}
</Layout>
);
}Trois détails valent la peine d'être relevés tout de suite.
action prend l'identifiant du logement en paramètre. C'est évident dit comme ça, et c'est pourtant l'erreur la plus fréquente : écrire onBook={() => action()} avec un identifiant en dur, et se retrouver avec six boutons qui réservent tous le même logement.
Chaque élément d'une liste porte une key. React s'en sert pour savoir quel élément a bougé quand la liste change. Sans elle, il vous le rappellera dans la console.
Et les dates sont des chaînes "2024-06-02", pas des Date. Notre domaine ne manipule plus d'instants depuis le chapitre 21 — c'est Stay.parse qui décide de ce qui est une date valide, et il le décide aussi bien dans le navigateur que sur le serveur.
Voilà "idéalement" la façon la plus simple d'écrire notre première page.
Remarquez au passage que notre application app est initialisée dans le module.
Il ne faudrait surtout pas faire cette initialisation dans nos fonctions loader ou action, car nos dépendances seraient réinitialisées à chaque fois !
On perdrait alors les effets de nos commandes : l'état serait constamment l'état initial.
Premier principe de React
Juste comme ça, cela ne fonctionne pas.
Car personne n'appelle la fonction render.
Alors aucune chance que quelque chose s'affiche !
Deuxième raison, React n'accepte pas de faire un render asynchrone, sauf dans un server component.
Laissons cette complexité de côté, car nous n'avons pas cette fonctionnalité disponible pour l'instant.
Dans React, la règle est simple : l'affichage est une fonction synchrone qui dépend d'un état.
UI = render(state)Un état est un objet, dont les changements sont observés par React.
Lorsqu'un changement survient, React appelle à nouveau la fonction render() pour déterminer ce qu'il faut afficher à l'écran.
Mais quel serait cet état dans notre cas ?
Le composant HomePage
On veut afficher la liste des logements accommodations lorsque le loader a terminé.
Mais en attendant ?
Habituellement, on affiche une page d'attente avec un spinner pour indiquer à l'utilisateur d'être patient.
C'est mieux qu'une page blanche !
Donc il nous faut un second état loading, qui vaut true si le loader est en train de s'exécuter, et false sinon.
Dans React, la déclaration d'un état se réalise par l'API useState.
Voici notre render modifié :
function render() {
// Nos 2 états
const [loading] = useState(false);
const [accommodations] = useState([]);
return (<Layout>
{accommodations.map(accommodation => <Accommodation accommodation={accommodation} onBook={() => action()}>)}
</Layout>)
}Problème suivant : nous n'appelons jamais notre loader().
Donc aucune possibilité de mettre à jour notre état accommodations ! La liste sera toujours vide.
Il nous faut donc déclarer un effet.
Un effet, ce n'est pas « une fonction externe au composant ». C'est un morceau de code que React exécute après avoir affiché le rendu, pour synchroniser le composant avec quelque chose qui vit en dehors de lui : une requête réseau, un abonnement, un timer.
Le tableau passé en second argument dit à React quand recommencer. Vide ([]), l'effet ne s'exécute qu'après le premier affichage.
Voici comment faire :
function HomePage() {
// 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 (
<Layout loading={loading}>
{accommodations.map((accommodation) => (
<Accommodation
key={accommodation.id}
accommodation={accommodation}
onBook={() => action(accommodation.id)}
/>
))}
</Layout>
);
}Une remarque en passant : en mode développement, React 18 exécute volontairement chaque effet deux fois. Ce n'est pas un bug, c'est un révélateur — il vous montre les effets qui ne supportent pas d'être rejoués. Le nôtre le supporte : il ne fait que recharger une liste. Un effet qui enverrait une commande, lui, poserait un vrai problème. Raison de plus pour ne jamais mettre de commande dans un loader.
Séparation UI et logique
Notre composant HomePage a un gros défaut : il mêle :
- la logique d'animation (lancer le chargement des données, afficher un spinner, puis la liste)
- le rendu graphique, avec les composants
<Layout />et<Accommodation />
Arrangeons cela avec un hook.
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 };
}Et allégeons notre composant afin qu'il s'occupe exclusivement de l'UI :
function HomePage() {
const { accommodations, loading } = useAccommodations();
return (
<Layout loading={loading}>
{accommodations.map((accommodation) => (
<Accommodation
key={accommodation.id}
accommodation={accommodation}
onBook={() => action(accommodation.id)}
/>
))}
</Layout>
);
}Voilà qui est bien séparé !
Reste à monter tout cela dans la page, dans packages/webapp/src/main.jsx — c'est le fichier que create-vite a généré pour nous :
import { createRoot } from "react-dom/client";
import { HomePage } from "./App";
createRoot(document.getElementById("root")).render(<HomePage />);Passons à l'étape suivante, pour créer nos 2 composants Layout et Accommodation.