Un composant React de choix d’image

Durant le développement de ma première application fullstack avec React (JavaScript « natif » d’abord puis TypeScript) et FastApi , j’ai été amené à créer un composant d’upload de fichier image.

Principe de fonctionnement

Ce composant est prévu pour être utilisé comme « champ » d’un formulaire, et utilise des données formattées comme suit :

{
   mode: 'current' | 'delete' | 'new', // utilisé côté back
   currentUrl: 'user-profile.png',
   file: objet File()
}

Au niveau visuel, d’un côté on affiche l’image actuelle ou celle par défaut ou la nouvelle image, de l’autre, des boutons radio permettant de soit garder l’image actuelle, soit l’effacer (et la remplacer par l’image par défaut), soit la remplacer.

Chacune des images mentionnées ci-dessus est affichée dans sa propre balise et est masquée ou affichée celon le besoin.

Le code

D’abord, on déclare le type pour le mode :

type AvatarMode = 'current' | 'delete' | 'new';

Ensuite, le type des données principales de ce composant :

interface AvatarState {
   mode: AvatarMode;
   currentUrl?: string;
   file?: File;
}

Notre composant prend 3 props :

  • la valeur actuelle (de type AvatarState)
  • une fonction fournie par le composant parent pour assurer la mise à jour des données
  • une URL de l’image par défaut
interface AvatarChooserProps {
   value: AvatarState;
   onChange: (data: AvatarState) => void;
   defaultAvatar?: string;
}

Au niveau des hooks React, j’en utilise 4 :

  • const fileInputRef = useRef(null); , pour référencer l’input de type « file »
  • un state booléen pour indiquer si la nouvelle image est bien chargée en mémoire : const [isNewImageLoaded, setIsNewImageLoaded] = useState(false);
  • un memo pour charger la nouvelle image en tant qu’objet URL :
    const previewUrl = useMemo(() => {
    return value.file ? URL.createObjectURL(value.file) : null;
    }, [value.file]);
  • un effect pour s’assurer de bien nettoyer l’objet URL au démontage du composant : useEffect(() => {
    return () => {
    if (previewUrl) {
    URL.revokeObjectURL(previewUrl);
    }
    };
    }, [previewUrl]);

Ensuite, deux handlers pour les champs radio et l’input de fichier :

const radioHandler = (e: React.ChangeEvent) => {
   onChange({ …value, mode: e.target.value as AvatarMode }); // on envoie directement la nouvelle valeur au composant parent
};
const fileHandler = (e: React.ChangeEvent) => {
   const file = e.target.files?.[0];
   if (!file) {
      return;
   }

   setIsNewImageLoaded(false);

   onChange({
      ...value,
      mode: 'new',
      file,
   });
};

Pour l’image actuelle, comme elle peut être déjà celle par défaut (pas d’image précise enregistrée en base), il faut une rapide logique pour la déterminer :

const currentAvatarUrl = value.currentUrl ?
   `${import.meta.env.VITE_APP_STATIC_FILES_URL}/${value.currentUrl`
   : defaultAvatar;

Ensuite, il ne reste plus qu’à calculer les flags pour l’image à afficher :

const showNew = value.mode === 'new' && previewUrl && isNewImageLoaded;
const showCurrent = value.mode === 'current' && !showNew;
const showDefault = value.mode === 'delete' || (value.mode === 'new' && !showNew);

Pour voir le code complet, RDV sur GitHub.

Laisser un commentaire