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 une 1è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 index.tsx
:
const app = new App(testDependencies())
async function loader () {
return await app.dependencies.bookings.getAvailableAccomodations({
from: toDate("2024-06-02"),
to: toDate("2024-06-04"),
});
}
async function action () {
const session = await app.run([
login({email:"faketenant@mail.com", password:"secret"}),
book({
accomodationId:"accomodation-1",
adults:2, children:3,
from : new Date("2024-06-02"),
to : new Date("2024-06-04"),
}),
]);
return session.error ? {status:"error", error:session.error} : {status:"ok"}
}
async function render(accomodations) {
return (<Layout>
{accomodations.map(accomodation => <Accomomdation accomodation={accomodation} onBook={() => action()}>)}
</Layout>)
}
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é-initilisé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 accomodations
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 2éme é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 [accomodations] = useState([]);
return (<Layout>
{accomodations.map(accomodation => <Accomodation accomodation={accomodation} onBook={() => action()}>)}
</Layout>)
}
Problème suivant : nous n'appelons jamais notre loader()
.
Donc aucune possibilité de mettre à jour notre état accomodations
! La liste sera toujours vide.
Il nous faut donc déclarer un effet. Il s'agit d'une fonction externe au composant, dont l'exécution modifie l'état du composant.
Voici comment faire :
function HomePage() {
// Nos 2 états sur la page
const [loading, setLoading] = useState(false);
const [accomodations, setAccomodations] = useState([]);
// Une fonction qui orchestre les changements d'état
const loadAccomodation = async () => {
setLoading(true);
setAccomodations(await loader());
setLoading(false);
}
// Un effet
useEffect( () => {
loadAccomodation()
} , [])
return (<Layout loading={loading}>
{accomodations.map(accomodation => <Accomodation
accomodation={accomodation}
onBook={() => action()}
>)}
</Layout>)
}
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<Accomomdation />
Arrangeons cela avec un hook.
function useAccomodations() {
// Nos 2 états sur la page
const [loading, setLoading] = useState(false);
const [accomodations, setAccomodations] = useState([]);
// Une fonction qui orchestre les changements d'état
const loadAccomodation = async () => {
setLoading(true);
setAccomodations(await loader());
setLoading(false);
}
// Un effet
useEffect( () => {
loadAccomodation()
} , [])
return {accomodations, loading}
}
Et allégeons notre composant afin qu'il s'occupe exclusivement de l'UI :
function HomePage() {
const {accomodations, loading} = useAccomodations()
return (<Layout loading={loading}>
{accomodations.map(accomodation => <Accomodation
accomodation={accomodation}
onBook={() => action()}
>)}
</Layout>)
}
Voilà qui est bien séparé !
Passons à l'étape suivante, pour créer nos 2 composants Layout
et Accomodation
.