📐 Construire une app en JS
23 Loader

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.