Aller au contenu principal

Images hébergées dans Supabase Storage

Héberger les illustrations des questions dans un bucket Supabase et les afficher à partir de leur adresse publique, même quand une image ne se charge pas.

Objectifs de la séance​

  • comprendre ce qu'est un bucket Supabase Storage et la différence entre un bucket public et privé ;
  • créer un bucket public avec un script SQL, puis y déposer les trois images ;
  • obtenir l'adresse publique d'une image avec getPublicUrl ;
  • afficher l'image avec expo-image, gérer l'échec avec onError ;
  • utiliser key pour qu'un composant réinitialise son état d'une question à l'autre.

Petite vérification​

Avant de commencer, vérifiez si vous savez...

  1. générer un quiz de 5 questions et lire le message affiché pendant que les questions se chargent
  2. citer le fichier qui contient la fonction convertirQuestion et dire ce qu'elle renvoie pour une question mal formée
  3. lancer npm test dans un terminal et lire, dans le résumé final, le nombre de tests en échec
📌 Réponse attendue

TP pour réfléchir et résoudre des problèmes​

Les illustrations des questions (cadenas.png, hamecon.png, wifi.png) sont pour l'instant embarquées dans l'application. Votre mission : les héberger dans Supabase et les télécharger à la demande. Les trois fichiers sont ceux que vous avez déjà dans le dossier assets/images/questions/ de votre projet (tutoriel 3) : vous allez les déposer sur Supabase.

Étape 1 — Créer un bucket public​

Créez le fichier sql/04_storage_images.sql. Un bucket est un « dossier » de fichiers hébergé par Supabase. Il est décrit par une ligne de la table storage.buckets. Complétez la ligne qui crée le bucket :

Collez le script dans le SQL Editor de Supabase et cliquez sur Run. Dans sql/README.md, ajoutez la ligne suivante à la fin du tableau des scripts :

| `04_storage_images.sql` | bucket public `images-questions` (illustrations) |

Bonne pratique - Un script rejouable

on conflict (id) do nothing signifie « si le bucket existe déjà, ne rien faire » : le script peut être exécuté deux fois sans erreur.

Étape 2 — Déposer les trois images​

Dans le tableau de bord, ouvrez Storage, puis le bucket images-questions, et utilisez le bouton d'envoi (Upload files) pour déposer cadenas.png, hamecon.png et wifi.png, depuis le dossier assets/images/questions/ de votre projet. Les noms doivent être exactement ceux de la colonne image_nom de la table question.

Vérifiez le résultat en ouvrant dans le navigateur l'adresse publique d'une image :

https://VOTRE_PROJET.supabase.co/storage/v1/object/public/images-questions/cadenas.png

Bonne pratique - Vérifier avant de coder

Ouvrir l'URL dans le navigateur prouve que le bucket est public et que le fichier porte le bon nom. Si l'image ne s'affiche pas ici, elle ne s'affichera pas dans l'application non plus : inutile de chercher le bug dans le code.

Étape 3 — Créer le service imagesService​

Créez src/services/imagesService.ts. La fonction urlImageQuestion construit l'adresse publique d'une image du bucket. C'est un simple calcul de texte : aucune requête n'est envoyée à ce moment.


Bonne pratique - Passer par un service, même pour un calcul

L'écran ne connaît ni le nom du bucket ni la façon dont Supabase construit ses adresses. Si vous changez un jour d'hébergeur d'images, seul ce fichier changera.

Étape 4 — Afficher l'image à partir de son adresse​

Dans src/components/IllustrationQuestion.tsx, l'image ne vient plus du dictionnaire des images locales : expo-image la télécharge à partir de l'adresse publique.


Bonne pratique - Laisser expo-image gérer le cache

expo-image mémorise les images déjà téléchargées : revoir la même illustration ne coûte aucune requête réseau. C'est une raison de plus de préférer expo-image au composant Image de React Native.

Étape 5 — Gérer l'échec du téléchargement​

Le réseau peut être coupé, ou le fichier absent du bucket. Dans ce cas, un cadre vide serait désagréable : le composant mémorise l'échec dans un état aEchoue, et n'affiche rien si l'image ne se charge pas.


Bonne pratique - Une image est un bonus, pas une condition

Si l'illustration manque, la question reste jouable. Une image qui ne se charge pas ne doit jamais bloquer le quiz ni afficher une erreur effrayante.

Étape 6 — Réinitialiser l'état avec key, et nettoyer​

Il reste un piège. React réutilise le composant IllustrationQuestion d'une question à la suivante (il est au même endroit dans l'écran). Si l'image de la question 1 échoue, aEchoue reste à true et l'illustration de la question 2 ne s'affichera jamais. Le remède : donner au composant une key propre à chaque question, ce qui crée un nouveau composant (avec un état remis à zéro).

Dans src/components/PartieEnCours.tsx, modifiez l'appel :

Supprimez ensuite le fichier src/data/imagesLocales.ts, devenu inutile (le dictionnaire de require).

Testez dans le navigateur :

  1. générez un quiz de 10 questions en « Tous » : les trois illustrations s'affichent, chargées depuis supabase.co (onglet Réseau de F12, filtre Img) ;
  2. revoyez une question illustrée : l'image vient du cache, sans nouvelle requête ;
  3. dans le tableau de bord, renommez temporairement wifi.png : la question concernée s'affiche sans image, la suivante garde la sienne. Remettez ensuite le bon nom.

Bonne pratique - key pour repartir de zéro

key ne sert pas qu'aux listes : changer la key d'un composant force React à le recréer avec un état neuf. C'est la solution propre pour réinitialiser l'état d'un composant quand son contenu change.

Étape 7 — Enregistrer et fusionner le tutoriel​

Le tutoriel 7 est terminé : fusionnez la branche. Sur GitHub, ouvrez la Pull Request de feature/donnees-async (relisez l'onglet Files changed, puis Merge pull request), puis mettez à jour votre dépôt local :

git switch main
git pull
git branch -d feature/donnees-async

Bonne pratique - Une Pull Request par tutoriel

Vous avez fait quatre commits dans ce tutoriel : la Pull Request les regroupe en une seule fonctionnalité relisible, « données asynchrones robustes ». main reste toujours une version qui fonctionne.

📌 Une solution

Notions théoriques​

Supabase Storage et les buckets​

Supabase Storage héberge des fichiers (images, PDF...). Ils sont rangés dans des buckets, un peu comme des dossiers de premier niveau. Chaque bucket est :

TypeAccès aux fichiersUsage
publictout le monde peut lire un fichier s'il connaît son adresseillustrations, logos
privéil faut une autorisation (règles d'accès) pour chaque lecturedocuments personnels

Les illustrations de questions ne contiennent aucune donnée sensible : un bucket public convient. Aucune règle d'écriture n'est créée : seul l'administrateur, depuis le tableau de bord, peut ajouter ou modifier des fichiers.

L'adresse publique d'un fichier​

L'adresse d'un fichier d'un bucket public suit toujours le même modèle :

https://VOTRE_PROJET.supabase.co/storage/v1/object/public/<bucket>/<nom du fichier>

La méthode getPublicUrl(nom) fabrique simplement cette adresse à partir du nom : elle n'envoie aucune requête et ne vérifie pas que le fichier existe. C'est l'image elle-même, quand elle est affichée, qui déclenche le téléchargement.

expo-image : source distante, cache, erreur​

Avec expo-image, une image distante se décrit par un objet { uri: "https://..." } (alors qu'une image embarquée utilise require(...)). Trois avantages :

  • un cache : une image déjà téléchargée est réutilisée ;
  • transition={200} : un fondu agréable à l'apparition ;
  • onError : une fonction appelée si le téléchargement échoue.

Les images locales : une limite​

Avec require("@/assets/images/questions/cadenas.png"), le chemin doit être écrit en toutes lettres : on ne peut pas le construire à partir d'un nom lu dans une base. C'est pourquoi l'application utilisait jusqu'ici un petit dictionnaire (imagesLocales.ts). Avec Storage, le nom de fichier venu de la base (image_nom) suffit : ajouter une illustration ne demande plus de republier l'application.

Pourquoi la key réinitialise l'état​

React identifie un composant par sa position dans l'arbre et par sa key. Si, d'une question à l'autre, IllustrationQuestion reste à la même position avec la même key (ou sans key), React le réutilise et conserve son état (aEchoue). Avec key={question.id}, chaque question a sa propre identité : changer de question détruit l'ancien composant et en crée un neuf, avec aEchoue à false.

L'état d'erreur qui reste d'une question à l'autre

C'est un bug typique : tout fonctionne avec la question 1, puis plus aucune image n'apparaît après qu'une seule a échoué. Dès qu'un composant garde un état lié à son contenu, pensez à lui donner une key.


Test de mémorisation/compréhension​


Qu'est-ce qu'un bucket dans Supabase Storage ?


Que fait getPublicUrl(nomImage) ?


Pourquoi le bucket images-questions peut-il être public ?


Que se passe-t-il quand le téléchargement d'une image échoue, avec notre composant ?


Pourquoi ajouter key={question.id} sur IllustrationQuestion ?


Pourquoi require("@/assets/images/questions/" + nom) est-il impossible ?



Exercices complémentaires​

Exercice A : un bucket privé​

Dans le tableau de bord, créez un second bucket, privé (sans l'option public). Déposez-y une image et essayez d'ouvrir son adresse publique dans le navigateur. Que constatez-vous ?

Exercice B : une image trop lente​

Dans F12 > Réseau, choisissez Slow 3G et générez un quiz. Observez le fondu (transition) à l'apparition des images. Que se passe-t-il si vous passez très vite à la question suivante ?


Ce qu'il faut retenir​

NotionRésumé
BucketConteneur de fichiers de Supabase Storage, public ou privé.
Bucket publicLisible par tous via une adresse publique ; adapté aux contenus non sensibles.
getPublicUrlFabrique l'adresse publique d'un fichier, sans requête réseau.
source={{ uri }}Façon de donner une image distante à expo-image (avec cache et transition).
onErrorPermet de réagir à un échec de chargement (ici : ne rien afficher).
keyIdentité d'un composant : en changer force React à le recréer avec un état neuf.
Pull RequestFusionne la branche du tutoriel 7 dans main après relecture.

Aperçu de la prochaine séance​

Votre application charge ses questions et ses images depuis le Cloud, et gère les lenteurs et les pannes. Avant d'aller plus loin, un TP autonome (facultatif) vous propose de jouer les relecteurs : vous analyserez du code asynchrone défectueux écrit par un stagiaire. Le tutoriel suivant s'intéressera ensuite aux joueurs et à la sécurité : identifier un joueur sans mot de passe, et empêcher la triche sur le score.