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 aveconError; - utiliser
keypour qu'un composant réinitialise son état d'une question à l'autre.
Petite vérification
Avant de commencer, vérifiez si vous savez...
- générer un quiz de 5 questions et lire le message affiché pendant que les questions se chargent
- citer le fichier qui contient la fonction
convertirQuestionet dire ce qu'elle renvoie pour une question mal formée - lancer
npm testdans 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) |
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
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.
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.
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.
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 :
- générez un quiz de 10 questions en « Tous » : les trois illustrations s'affichent, chargées depuis
supabase.co(onglet Réseau deF12, filtre Img) ; - revoyez une question illustrée : l'image vient du cache, sans nouvelle requête ;
- 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.
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
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 :
| Type | Accès aux fichiers | Usage |
|---|---|---|
| public | tout le monde peut lire un fichier s'il connaît son adresse | illustrations, logos |
| privé | il faut une autorisation (règles d'accès) pour chaque lecture | documents 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.
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
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
| Notion | Résumé |
|---|---|
| Bucket | Conteneur de fichiers de Supabase Storage, public ou privé. |
| Bucket public | Lisible par tous via une adresse publique ; adapté aux contenus non sensibles. |
getPublicUrl | Fabrique 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). |
onError | Permet de réagir à un échec de chargement (ici : ne rien afficher). |
key | Identité d'un composant : en changer force React à le recréer avec un état neuf. |
| Pull Request | Fusionne 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.