{"id":1365,"date":"2026-06-23T14:37:54","date_gmt":"2026-06-23T13:37:54","guid":{"rendered":"https:\/\/geekcommunicant.com\/blog\/?p=1365"},"modified":"2026-06-23T14:38:14","modified_gmt":"2026-06-23T13:38:14","slug":"composant-react-choix-image","status":"publish","type":"post","link":"https:\/\/geekcommunicant.com\/blog\/2026\/06\/23\/composant-react-choix-image\/","title":{"rendered":"Un composant React de choix d&rsquo;image"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Durant le d\u00e9veloppement de ma premi\u00e8re application fullstack avec React (JavaScript \u00ab\u00a0natif\u00a0\u00bb d&rsquo;abord puis TypeScript) et FastApi , j&rsquo;ai \u00e9t\u00e9 amen\u00e9 \u00e0 cr\u00e9er un composant d&rsquo;upload de fichier image.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Principe de fonctionnement<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ce composant est pr\u00e9vu pour \u00eatre utilis\u00e9 comme \u00ab\u00a0champ\u00a0\u00bb d&rsquo;un formulaire, et utilise des donn\u00e9es formatt\u00e9es comme suit :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\n{\n   mode: &#039;current&#039; | &#039;delete&#039; | &#039;new&#039;, \/\/ utilis\u00e9 c\u00f4t\u00e9 back\n   currentUrl: &#039;user-profile.png&#039;,\n   file: objet File()\n}\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\">Au niveau visuel, d&rsquo;un c\u00f4t\u00e9 on affiche l&rsquo;image actuelle ou celle par d\u00e9faut ou la nouvelle image, de l&rsquo;autre, des boutons radio permettant de soit garder l&rsquo;image actuelle, soit l&rsquo;effacer (et la remplacer par l&rsquo;image par d\u00e9faut), soit la remplacer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Chacune des images mentionn\u00e9es ci-dessus est affich\u00e9e dans sa propre balise <img> et est masqu\u00e9e ou affich\u00e9e celon le besoin.<\/p>\n\n\n\n<!--more-->\n\n\n\n<h2 class=\"wp-block-heading\">Le code<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">D&rsquo;abord, on d\u00e9clare le type pour le mode :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\ntype AvatarMode = &#039;current&#039; | &#039;delete&#039; | &#039;new&#039;;\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\">Ensuite, le type des donn\u00e9es principales de ce composant :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\ninterface AvatarState {\n   mode: AvatarMode;\n   currentUrl?: string;\n   file?: File;\n}\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\">Notre composant prend 3 props :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>la valeur actuelle (de type <code>AvatarState<\/code>)<\/li>\n\n\n\n<li>une fonction fournie par le composant parent pour assurer la mise \u00e0 jour des donn\u00e9es<\/li>\n\n\n\n<li>une URL de l&rsquo;image par d\u00e9faut<\/li>\n<\/ul>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\ninterface AvatarChooserProps {\n   value: AvatarState;\n   onChange: (data: AvatarState) =&gt; void;\n   defaultAvatar?: string;\n}\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\">Au niveau des hooks React, j&rsquo;en utilise 4 :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>const fileInputRef = useRef(null); <\/code>, pour r\u00e9f\u00e9rencer l&rsquo;input de type \u00ab\u00a0file\u00a0\u00bb<\/li>\n\n\n\n<li>un state bool\u00e9en pour indiquer si la nouvelle image est bien charg\u00e9e en m\u00e9moire : <code>const [isNewImageLoaded, setIsNewImageLoaded] = useState(false);<\/code><\/li>\n\n\n\n<li>un memo pour charger la nouvelle image en tant qu&rsquo;objet URL :<br><code>const previewUrl = useMemo(() => {<br>   return value.file ? URL.createObjectURL(value.file) : null;<br>}, [value.file]);<\/code><\/li>\n\n\n\n<li>un effect pour s&rsquo;assurer de bien nettoyer l&rsquo;objet URL au d\u00e9montage du composant : <code>useEffect(() => {<br>   return () => {<br>      if (previewUrl) {<br>         URL.revokeObjectURL(previewUrl);<br>      }<br>   };<br>}, [previewUrl]);<\/code><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Ensuite, deux handlers pour les champs radio et l&rsquo;input de fichier :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\nconst radioHandler = (e: React.ChangeEvent) =&gt; {\n   onChange({ \u2026value, mode: e.target.value as AvatarMode }); \/\/ on envoie directement la nouvelle valeur au composant parent\n};\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nconst fileHandler = (e: React.ChangeEvent) =&gt; {\n   const file = e.target.files?.&#x5B;0];\n   if (!file) {\n      return;\n   }\n\n   setIsNewImageLoaded(false);\n\n   onChange({\n      ...value,\n      mode: &#039;new&#039;,\n      file,\n   });\n};\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\">Pour l&rsquo;image actuelle, comme elle peut \u00eatre d\u00e9j\u00e0 celle par d\u00e9faut (pas d&rsquo;image pr\u00e9cise enregistr\u00e9e en base), il faut une rapide logique pour la d\u00e9terminer :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\nconst currentAvatarUrl = value.currentUrl ?\n   `${import.meta.env.VITE_APP_STATIC_FILES_URL}\/${value.currentUrl`\n   : defaultAvatar;\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\">Ensuite, il ne reste plus qu&rsquo;\u00e0 calculer les flags pour l&rsquo;image \u00e0 afficher :<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\nconst showNew = value.mode === &#039;new&#039; &amp;&amp; previewUrl &amp;&amp; isNewImageLoaded;\nconst showCurrent = value.mode === &#039;current&#039; &amp;&amp; !showNew;\nconst showDefault = value.mode === &#039;delete&#039; || (value.mode === &#039;new&#039; &amp;&amp; !showNew);\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\">Pour voir le code complet, RDV sur <a href=\"https:\/\/github.com\/valvalsal\/fullstack-todo-fastapi-react\/blob\/main\/frontend\/src\/components\/AvatarChooser\/index.tsx\">GitHub<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Durant le d\u00e9veloppement de ma premi\u00e8re application fullstack avec React (JavaScript \u00ab\u00a0natif\u00a0\u00bb d&rsquo;abord puis TypeScript) et FastApi , j&rsquo;ai \u00e9t\u00e9 amen\u00e9 \u00e0 cr\u00e9er un composant d&rsquo;upload de fichier image. Principe de fonctionnement Ce composant est pr\u00e9vu pour \u00eatre utilis\u00e9 comme &hellip; <a href=\"https:\/\/geekcommunicant.com\/blog\/2026\/06\/23\/composant-react-choix-image\/\">Continuer la lecture <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":true,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"jetpack_post_was_ever_published":false},"categories":[19],"tags":[76,78],"class_list":["post-1365","post","type-post","status-publish","format-standard","hentry","category-javascript","tag-react","tag-typescript"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/p2IoOb-m1","jetpack_sharing_enabled":true,"jetpack_likes_enabled":true,"jetpack-related-posts":[{"id":1138,"url":"https:\/\/geekcommunicant.com\/blog\/2023\/03\/17\/quel-est-le-plus-vieux-navigateur\/","url_meta":{"origin":1365,"position":0},"title":"Quel est le plus vieux navigateur ?","author":"geekc","date":"17\/03\/2023","format":false,"excerpt":"Derni\u00e8rement, je me suis pos\u00e9 la question de quel est le plus vieux navigateur web toujours en cours de d\u00e9veloppement. J\u2019ai donc fait quelques recherches que je vais essayer de vous synth\u00e9tiser au mieux ici. Pr\u00e9sentation des candidats J\u2019ai orient\u00e9 mes recherches autour des 5 navigateurs les plus utilis\u00e9s actuellement,\u2026","rel":"","context":"Dans &quot;Divers&quot;","block_context":{"text":"Divers","link":"https:\/\/geekcommunicant.com\/blog\/category\/divers\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":382,"url":"https:\/\/geekcommunicant.com\/blog\/2014\/08\/15\/menu-mobile-modx\/","url_meta":{"origin":1365,"position":1},"title":"Un menu pour mobile avec MODX","author":"geekc","date":"15\/08\/2014","format":false,"excerpt":"Avec la g\u00e9n\u00e9ralisation des terminaux mobiles (smartphones et tablettes) il est important de prendre en compte la taille r\u00e9duite de leur affichage lors de la conception de votre site. Un \u00e9l\u00e9ment essentiel du design d'un site est le menu principal de navigation. Pour les terminaux mobiles, une des solutions est\u2026","rel":"","context":"Dans &quot;JavaScript&quot;","block_context":{"text":"JavaScript","link":"https:\/\/geekcommunicant.com\/blog\/category\/javascript\/"},"img":{"alt_text":"Menu pour smartphone","src":"https:\/\/i0.wp.com\/geekcommunicant.com\/blog\/wp-content\/uploads\/menu-smartphone.jpg?resize=350%2C200&ssl=1","width":350,"height":200},"classes":[]},{"id":1143,"url":"https:\/\/geekcommunicant.com\/blog\/2023\/03\/24\/gloubi-script-horloge-illustree\/","url_meta":{"origin":1365,"position":2},"title":"Gloubi script &#8211; une horloge illustr\u00e9e","author":"geekc","date":"24\/03\/2023","format":false,"excerpt":"Aujourd\u2019hui, j\u2019ai d\u00e9cid\u00e9 de m\u2019attaquer \u00e0 la version gloubi d\u2019un script que j\u2019ai \u00e9galement d\u00e9j\u00e0 cr\u00e9\u00e9 de mon c\u00f4t\u00e9\u00a0: l\u2019horloge illustr\u00e9e. La version gloubi est visible ici, tandis que la cr\u00e9ation de ma propre version, faite avec jQuery, est pr\u00e9sent\u00e9e ici. Pr\u00e9sentation de la version gloubi Pour commencer, on cr\u00e9\u00e9\u2026","rel":"","context":"Dans &quot;JavaScript&quot;","block_context":{"text":"JavaScript","link":"https:\/\/geekcommunicant.com\/blog\/category\/javascript\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":1324,"url":"https:\/\/geekcommunicant.com\/blog\/2025\/12\/28\/mon-experience-avec-linux\/","url_meta":{"origin":1365,"position":3},"title":"Mon exp\u00e9rience avec Linux","author":"geekc","date":"28\/12\/2025","format":false,"excerpt":"Avec la fin de support de Windows 10 en octobre dernier, Linux a vu son usage progresser fortement cette ann\u00e9e. Voici mon histoire avec le pingouin. Linux dans mon pass\u00e9 De mon c\u00f4t\u00e9, je n'ai pas attendu cet \u00e9v\u00e9nement pour m'int\u00e9resser \u00e0 Linux, vu que j'avais d\u00e9j\u00e0 utilis\u00e9 courament Ubuntu\u2026","rel":"","context":"Dans &quot;Divers&quot;","block_context":{"text":"Divers","link":"https:\/\/geekcommunicant.com\/blog\/category\/divers\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":1011,"url":"https:\/\/geekcommunicant.com\/blog\/2022\/12\/23\/degrades-css\/","url_meta":{"origin":1365,"position":4},"title":"Les d\u00e9grad\u00e9s en CSS","author":"geekc","date":"23\/12\/2022","format":false,"excerpt":"Comme sous-entendu dans un pr\u00e9c\u00e9dent gloubi tuto, il est maintenant possible avec CSS3 de cr\u00e9er des d\u00e9grad\u00e9s de couleurs. Pour CSS, les d\u00e9grad\u00e9s sont consid\u00e9r\u00e9s comme des images, et donc doivent \u00eatre utilis\u00e9s dans l\u2019attribut background ou background-image d\u2019une d\u00e9claration de style. Il existe en CSS 3 types de d\u00e9grad\u00e9s\u2026","rel":"","context":"Dans &quot;CSS&quot;","block_context":{"text":"CSS","link":"https:\/\/geekcommunicant.com\/blog\/category\/css\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":736,"url":"https:\/\/geekcommunicant.com\/blog\/2019\/04\/22\/theme-enfant-wordpress\/","url_meta":{"origin":1365,"position":5},"title":"Cr\u00e9er un th\u00e8me enfant pour WordPress","author":"geekc","date":"22\/04\/2019","format":false,"excerpt":"Un th\u00e8me enfant est un th\u00e8me reprenant toutes les fonctionnalit\u00e9s d'un th\u00e8me existant, qu'on appelera \"th\u00e8me parent\". Pourquoi cr\u00e9er un th\u00e8me enfant ? Les th\u00e8mes WordPress disponibles offrent d\u00e9j\u00e0 un large choix d'options pour les personnaliser. Mais, par exemple, disons que vous voulez modifier la couleur d'un ou plusieurs \u00e9l\u00e9ments,\u2026","rel":"","context":"Dans &quot;WordPress&quot;","block_context":{"text":"WordPress","link":"https:\/\/geekcommunicant.com\/blog\/category\/wordpress\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]}],"_links":{"self":[{"href":"https:\/\/geekcommunicant.com\/blog\/wp-json\/wp\/v2\/posts\/1365","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/geekcommunicant.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/geekcommunicant.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/geekcommunicant.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/geekcommunicant.com\/blog\/wp-json\/wp\/v2\/comments?post=1365"}],"version-history":[{"count":1,"href":"https:\/\/geekcommunicant.com\/blog\/wp-json\/wp\/v2\/posts\/1365\/revisions"}],"predecessor-version":[{"id":1366,"href":"https:\/\/geekcommunicant.com\/blog\/wp-json\/wp\/v2\/posts\/1365\/revisions\/1366"}],"wp:attachment":[{"href":"https:\/\/geekcommunicant.com\/blog\/wp-json\/wp\/v2\/media?parent=1365"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/geekcommunicant.com\/blog\/wp-json\/wp\/v2\/categories?post=1365"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/geekcommunicant.com\/blog\/wp-json\/wp\/v2\/tags?post=1365"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}