[{"data":1,"prerenderedAt":879},["ShallowReactive",2],{"post-\u002Ffr\u002Fposts\u002Fusing-pinia-colada-in-modals-without-spoiling-the-ux":3},{"id":4,"title":5,"body":6,"contentId":867,"date":868,"description":869,"extension":870,"meta":871,"navigation":62,"path":872,"rawbody":873,"robots":874,"schemaOrg":874,"seo":875,"sitemap":876,"stem":877,"__hash__":878},"posts\u002Ffr\u002Fposts\u002Fusing-pinia-colada-in-modals-without-spoiling-the-ux.md","Pinia Colada, les modales et l'expérience utilisateur",{"type":7,"value":8,"toc":861},"minimark",[9,24,41,47,52,55,73,76,79,87,90,96,380,387,409,415,489,498,508,512,515,531,537,541,557,563,566,569,809,821,831,835,838,846,852,857],[10,11,12,13,23],"p",{},"Depuis plusieurs mois, j'utilise ",[14,15,19],"a",{"href":16,"rel":17},"https:\u002F\u002Fpinia-colada.esm.dev\u002F",[18],"nofollow",[20,21,22],"strong",{},"Pinia Colada",". Je l'ai découvert en recherchant une solution élégante pour récupérer des données sur mon site personnel (celui que vous lisez actuellement). Depuis, je l'ai utilisé dans tous mes projets qui nécessitent des données d'une API. C'est facile à utiliser, ça fonctionne parfaitement et cela me permet de créer de meilleures expériences grâce à sa mise en cache intégrée, sa fonctionnalité de revalidation des données en arrière-plan, et la facilité de mettre en œuvre des optimistic updates.",[10,25,26,28,29,36,37,40],{},[20,27,22],{}," est une couche de récupération de données pour ",[14,30,33],{"href":31,"rel":32},"https:\u002F\u002Fpinia.vuejs.org\u002F",[18],[20,34,35],{},"Pinia",", le store pour ",[20,38,39],{},"Vue.js",", rendant la gestion d'état asynchrone d'une simplicité enfantine. Vous devriez vraiment l'essayer. Je ne peux plus commencer un nouveau projet sans lui désormais.",[10,42,43,44,46],{},"Cependant, j'ai récemment rencontré une difficulté en utilisant ",[20,45,22],{}," dans une modale. Laissez-moi vous expliquer le problème et comment je l'ai résolu.",[48,49,51],"h2",{"id":50},"le-problème","Le Problème",[10,53,54],{},"Avant de me plonger dans l'explication, regardez la vidéo suivante :",[56,57,58,59,58,69],"figure",{},"\n  ",[60,61,63,64,58],"video",{"autoPlay":62,"loop":62,"muted":62,"playsInline":62},true,"\n    ",[65,66],"source",{"src":67,"type":68},"https:\u002F\u002Fimages.soubiran.dev\u002Fposts\u002Fusing-pinia-colada-in-modals-without-spoiling-the-ux\u002Fthe-problem.mp4","video\u002Fmp4",[70,71,72],"figcaption",{},"La modale se ferme avant que les données ne soient mises à jour.",[10,74,75],{},"Vous voyez le problème ?",[10,77,78],{},"Lorsque je soumets le formulaire dans la modale, le bouton \"Soumettre\" est désactivé et un indicateur de chargement apparaît. Jusqu'ici, tout va bien. Mais ensuite, la modale se ferme avant que les données ne soient mises à jour sur la page. L'utilisateur doit attendre, mais aucun retour ne s'affiche, ce qui nuit à l'expérience utilisateur. L'utilisateur ne sait pas si l'action a été réussie.",[10,80,81,82],{},"Vous pouvez trouver le code de cette vidéo sur ",[83,84,86],"git-hub-link",{"repo":85},"barbapapazes\u002Fpinia-colada-await-invalidate-queries",".",[10,88,89],{},"Plongeons dans les détails techniques du problème et ses causes.",[10,91,92,93,95],{},"Le code ",[20,94,22],{}," que j'ai écrit pour muter les données est le suivant :",[97,98,103],"pre",{"className":99,"code":100,"language":101,"meta":102,"style":102},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","const open = ref(false)\nconst { mutate: createComment, isLoading: isCreatingComment } = useMutation({\n  mutation: () => $fetch('\u002Fapi\u002Fcomments', {\n    method: 'POST',\n    body: comment.value\n  }),\n  onSettled: () => {\n    queryCache.invalidateQueries({ key: ['comments'] })\n  },\n  onSuccess: () => {\n    comment.value.text = ''\n    open.value = false\n  }\n})\n","ts","",[104,105,106,137,180,213,232,248,259,273,311,317,331,352,367,373],"code",{"__ignoreMap":102},[107,108,111,115,119,123,127,130,134],"span",{"class":109,"line":110},"line",1,[107,112,114],{"class":113},"spNyl","const",[107,116,118],{"class":117},"sTEyZ"," open ",[107,120,122],{"class":121},"sMK4o","=",[107,124,126],{"class":125},"s2Zo4"," ref",[107,128,129],{"class":117},"(",[107,131,133],{"class":132},"sfNiH","false",[107,135,136],{"class":117},")\n",[107,138,140,142,145,149,152,155,158,161,163,166,169,172,175,177],{"class":109,"line":139},2,[107,141,114],{"class":113},[107,143,144],{"class":121}," {",[107,146,148],{"class":147},"swJcz"," mutate",[107,150,151],{"class":121},":",[107,153,154],{"class":117}," createComment",[107,156,157],{"class":121},",",[107,159,160],{"class":147}," isLoading",[107,162,151],{"class":121},[107,164,165],{"class":117}," isCreatingComment ",[107,167,168],{"class":121},"}",[107,170,171],{"class":121}," =",[107,173,174],{"class":125}," useMutation",[107,176,129],{"class":117},[107,178,179],{"class":121},"{\n",[107,181,183,186,188,191,194,197,199,202,206,208,210],{"class":109,"line":182},3,[107,184,185],{"class":125},"  mutation",[107,187,151],{"class":121},[107,189,190],{"class":121}," ()",[107,192,193],{"class":113}," =>",[107,195,196],{"class":125}," $fetch",[107,198,129],{"class":117},[107,200,201],{"class":121},"'",[107,203,205],{"class":204},"sfazB","\u002Fapi\u002Fcomments",[107,207,201],{"class":121},[107,209,157],{"class":121},[107,211,212],{"class":121}," {\n",[107,214,216,219,221,224,227,229],{"class":109,"line":215},4,[107,217,218],{"class":147},"    method",[107,220,151],{"class":121},[107,222,223],{"class":121}," '",[107,225,226],{"class":204},"POST",[107,228,201],{"class":121},[107,230,231],{"class":121},",\n",[107,233,235,238,240,243,245],{"class":109,"line":234},5,[107,236,237],{"class":147},"    body",[107,239,151],{"class":121},[107,241,242],{"class":117}," comment",[107,244,86],{"class":121},[107,246,247],{"class":117},"value\n",[107,249,251,254,257],{"class":109,"line":250},6,[107,252,253],{"class":121},"  }",[107,255,256],{"class":117},")",[107,258,231],{"class":121},[107,260,262,265,267,269,271],{"class":109,"line":261},7,[107,263,264],{"class":125},"  onSettled",[107,266,151],{"class":121},[107,268,190],{"class":121},[107,270,193],{"class":113},[107,272,212],{"class":121},[107,274,276,279,281,284,286,289,292,294,297,299,302,304,307,309],{"class":109,"line":275},8,[107,277,278],{"class":117},"    queryCache",[107,280,86],{"class":121},[107,282,283],{"class":125},"invalidateQueries",[107,285,129],{"class":147},[107,287,288],{"class":121},"{",[107,290,291],{"class":147}," key",[107,293,151],{"class":121},[107,295,296],{"class":147}," [",[107,298,201],{"class":121},[107,300,301],{"class":204},"comments",[107,303,201],{"class":121},[107,305,306],{"class":147},"] ",[107,308,168],{"class":121},[107,310,136],{"class":147},[107,312,314],{"class":109,"line":313},9,[107,315,316],{"class":121},"  },\n",[107,318,320,323,325,327,329],{"class":109,"line":319},10,[107,321,322],{"class":125},"  onSuccess",[107,324,151],{"class":121},[107,326,190],{"class":121},[107,328,193],{"class":113},[107,330,212],{"class":121},[107,332,334,337,339,342,344,347,349],{"class":109,"line":333},11,[107,335,336],{"class":117},"    comment",[107,338,86],{"class":121},[107,340,341],{"class":117},"value",[107,343,86],{"class":121},[107,345,346],{"class":117},"text",[107,348,171],{"class":121},[107,350,351],{"class":121}," ''\n",[107,353,355,358,360,362,364],{"class":109,"line":354},12,[107,356,357],{"class":117},"    open",[107,359,86],{"class":121},[107,361,341],{"class":117},[107,363,171],{"class":121},[107,365,366],{"class":132}," false\n",[107,368,370],{"class":109,"line":369},13,[107,371,372],{"class":121},"  }\n",[107,374,376,378],{"class":109,"line":375},14,[107,377,168],{"class":121},[107,379,136],{"class":117},[10,381,382,383,386],{},"La variable ",[104,384,385],{},"open"," contrôle la visibilité de la modale.",[10,388,389,390,393,394,397,398,401,402,405,406,408],{},"L'utilisateur ouvre la modale, remplit le formulaire et clique sur le bouton \"Soumettre\". La mutation ",[104,391,392],{},"createComment"," est alors appelée, envoyant des données à l'API via la fonction ",[104,395,396],{},"mutate",". En cas de succès, la fonction ",[104,399,400],{},"onSuccess"," réinitialise le formulaire et ferme la modale. Enfin, la fonction ",[104,403,404],{},"onSettled"," invalide la requête ",[104,407,301],{}," pour récupérer à nouveau les données de l'API.",[10,410,411,412,414],{},"La requête ",[20,413,22],{}," ressemble à ceci :",[97,416,418],{"className":99,"code":417,"language":101,"meta":102,"style":102},"const { state } = useQuery({\n  key: ['comments'],\n  query: () => $fetch('\u002Fapi\u002Fcomments')\n})\n",[104,419,420,440,460,483],{"__ignoreMap":102},[107,421,422,424,426,429,431,433,436,438],{"class":109,"line":110},[107,423,114],{"class":113},[107,425,144],{"class":121},[107,427,428],{"class":117}," state ",[107,430,168],{"class":121},[107,432,171],{"class":121},[107,434,435],{"class":125}," useQuery",[107,437,129],{"class":117},[107,439,179],{"class":121},[107,441,442,445,447,449,451,453,455,458],{"class":109,"line":139},[107,443,444],{"class":147},"  key",[107,446,151],{"class":121},[107,448,296],{"class":117},[107,450,201],{"class":121},[107,452,301],{"class":204},[107,454,201],{"class":121},[107,456,457],{"class":117},"]",[107,459,231],{"class":121},[107,461,462,465,467,469,471,473,475,477,479,481],{"class":109,"line":182},[107,463,464],{"class":125},"  query",[107,466,151],{"class":121},[107,468,190],{"class":121},[107,470,193],{"class":113},[107,472,196],{"class":125},[107,474,129],{"class":117},[107,476,201],{"class":121},[107,478,205],{"class":204},[107,480,201],{"class":121},[107,482,136],{"class":117},[107,484,485,487],{"class":109,"line":215},[107,486,168],{"class":121},[107,488,136],{"class":117},[10,490,491,492,494,495,497],{},"Le problème est que ",[104,493,404],{},", qui invalide la requête et récupère à nouveau les données, est appelé après ",[104,496,400],{},", ce qui ferme la modale. Par conséquent, l'utilisateur voit la modale se fermer avant que les données n'apparaissent sur la page, ce qui crée de la confusion. \"Où est mon commentaire ? Ça a fonctionné ?\"",[10,499,500,501,504,505,507],{},"Serait-il possible de déplacer ",[104,502,503],{},"open.value = false"," vers ",[104,506,404],{},", après l'invalidation de la requête ? Malheureusement, non.",[48,509,511],{"id":510},"solutions-multiples","Solutions Multiples",[10,513,514],{},"Pour résoudre le problème, deux solutions me sont venues à l'esprit :",[516,517,518,525],"ul",{},[519,520,521,524],"li",{},[20,522,523],{},"Mises à Jour Optimistes"," : C'est la meilleure solution pour l'expérience utilisateur, mettant immédiatement à jour l'interface, mais cela nécessite un code plus complexe. Ce n'est pas toujours faisable, surtout si l'API renvoie des données différentes de celles qui ont été envoyées. Par exemple, avec un système de commentaire permettant le markdown, l'API renvoie le markdown formaté, mais l'utilisateur envoie du markdown brut. Dans de tels cas, les mises à jour optimistes ne sont pas viables, et un état de chargement est la seule solution.",[519,526,527,530],{},[20,528,529],{},"État de Chargement"," : C'est la solution la plus simple. Désactiver le bouton \"Soumettre\", afficher un indicateur de chargement et attendre la réponse de l'API. C'est la plus commune et la plus facile à mettre en œuvre. Cependant, dans une modale, cela nécessite des ajustements par rapport à un formulaire mis en ligne dans une page.",[10,532,533,534,536],{},"Pour mon projet, les mises à jour optimistes n'étaient pas une option, j'ai donc dû utiliser l'état de chargement. Pourtant, lors de sa mise en œuvre, j'ai rencontré ce problème. J'ai beau avoir lu la documentation ",[20,535,22],{}," plusieurs fois, je n'avais pas de solution. À un moment, une idée a fini par germer. J'ai sauté dans la documentation pour voir si ma compréhension pouvait être une solution viable.",[48,538,540],{"id":539},"attendre-les-requêtes-invalidées","Attendre les Requêtes Invalidées",[10,542,543,544,556],{},"La documentation comprend une section sur ",[14,545,548,549,552,553,555],{"href":546,"rel":547},"https:\u002F\u002Fpinia-colada.esm.dev\u002Fguide\u002Fquery-invalidation.html#To-await-or-not-to-await",[18],"\"To ",[104,550,551],{},"await"," or not to ",[104,554,551],{},"\""," qui indique :",[558,559,560],"blockquote",{},[10,561,562],{},"In mutations, it's possible to await within the different hooks. This will effectively delay the resolution or rejection of the mutation and its asyncStatus.",[10,564,565],{},"C'était la clé. Je n'avais jamais compris le cas d'usage de l'attente des requêtes invalidées, mais c'est devenu clair avec ce problème. En attendant les requêtes invalidées, je pouvais retarder la fermeture de la modale jusqu'à ce que les données soient récupérées et disponibles sur la page.",[10,567,568],{},"Voici le code pour mettre en œuvre cette solution :",[97,570,572],{"className":99,"code":571,"language":101,"meta":102,"style":102},"const open = ref(false)\nconst { mutate: createComment, isLoading: isCreatingComment } = useMutation({\n  mutation: () => $fetch('\u002Fapi\u002Fcomments', {\n    method: 'POST',\n    body: comment.value\n  }),\n  onSettled: async (_, error) => {\n    await queryCache.invalidateQueries({ key: ['comments'] })\n\n    if (!error) {\n      comment.value.text = ''\n      open.value = false\n    }\n  },\n})\n",[104,573,574,590,620,644,658,670,678,705,740,745,763,780,793,798,802],{"__ignoreMap":102},[107,575,576,578,580,582,584,586,588],{"class":109,"line":110},[107,577,114],{"class":113},[107,579,118],{"class":117},[107,581,122],{"class":121},[107,583,126],{"class":125},[107,585,129],{"class":117},[107,587,133],{"class":132},[107,589,136],{"class":117},[107,591,592,594,596,598,600,602,604,606,608,610,612,614,616,618],{"class":109,"line":139},[107,593,114],{"class":113},[107,595,144],{"class":121},[107,597,148],{"class":147},[107,599,151],{"class":121},[107,601,154],{"class":117},[107,603,157],{"class":121},[107,605,160],{"class":147},[107,607,151],{"class":121},[107,609,165],{"class":117},[107,611,168],{"class":121},[107,613,171],{"class":121},[107,615,174],{"class":125},[107,617,129],{"class":117},[107,619,179],{"class":121},[107,621,622,624,626,628,630,632,634,636,638,640,642],{"class":109,"line":182},[107,623,185],{"class":125},[107,625,151],{"class":121},[107,627,190],{"class":121},[107,629,193],{"class":113},[107,631,196],{"class":125},[107,633,129],{"class":117},[107,635,201],{"class":121},[107,637,205],{"class":204},[107,639,201],{"class":121},[107,641,157],{"class":121},[107,643,212],{"class":121},[107,645,646,648,650,652,654,656],{"class":109,"line":215},[107,647,218],{"class":147},[107,649,151],{"class":121},[107,651,223],{"class":121},[107,653,226],{"class":204},[107,655,201],{"class":121},[107,657,231],{"class":121},[107,659,660,662,664,666,668],{"class":109,"line":234},[107,661,237],{"class":147},[107,663,151],{"class":121},[107,665,242],{"class":117},[107,667,86],{"class":121},[107,669,247],{"class":117},[107,671,672,674,676],{"class":109,"line":250},[107,673,253],{"class":121},[107,675,256],{"class":117},[107,677,231],{"class":121},[107,679,680,682,684,687,690,694,696,699,701,703],{"class":109,"line":261},[107,681,264],{"class":125},[107,683,151],{"class":121},[107,685,686],{"class":113}," async",[107,688,689],{"class":121}," (",[107,691,693],{"class":692},"sHdIc","_",[107,695,157],{"class":121},[107,697,698],{"class":692}," error",[107,700,256],{"class":121},[107,702,193],{"class":113},[107,704,212],{"class":121},[107,706,707,711,714,716,718,720,722,724,726,728,730,732,734,736,738],{"class":109,"line":275},[107,708,710],{"class":709},"s7zQu","    await",[107,712,713],{"class":117}," queryCache",[107,715,86],{"class":121},[107,717,283],{"class":125},[107,719,129],{"class":147},[107,721,288],{"class":121},[107,723,291],{"class":147},[107,725,151],{"class":121},[107,727,296],{"class":147},[107,729,201],{"class":121},[107,731,301],{"class":204},[107,733,201],{"class":121},[107,735,306],{"class":147},[107,737,168],{"class":121},[107,739,136],{"class":147},[107,741,742],{"class":109,"line":313},[107,743,744],{"emptyLinePlaceholder":62},"\n",[107,746,747,750,752,755,758,761],{"class":109,"line":319},[107,748,749],{"class":709},"    if",[107,751,689],{"class":147},[107,753,754],{"class":121},"!",[107,756,757],{"class":117},"error",[107,759,760],{"class":147},") ",[107,762,179],{"class":121},[107,764,765,768,770,772,774,776,778],{"class":109,"line":333},[107,766,767],{"class":117},"      comment",[107,769,86],{"class":121},[107,771,341],{"class":117},[107,773,86],{"class":121},[107,775,346],{"class":117},[107,777,171],{"class":121},[107,779,351],{"class":121},[107,781,782,785,787,789,791],{"class":109,"line":354},[107,783,784],{"class":117},"      open",[107,786,86],{"class":121},[107,788,341],{"class":117},[107,790,171],{"class":121},[107,792,366],{"class":132},[107,794,795],{"class":109,"line":369},[107,796,797],{"class":121},"    }\n",[107,799,800],{"class":109,"line":375},[107,801,316],{"class":121},[107,803,805,807],{"class":109,"line":804},15,[107,806,168],{"class":121},[107,808,136],{"class":117},[10,810,811,812,814,815,817,818,820],{},"Au lieu d'utiliser ",[104,813,400],{}," pour fermer la modale, je l'ai déplacé vers ",[104,816,404],{},", ajoutant un ",[104,819,551],{}," avant l'invalidation du cache. Ainsi, la modale ne se ferme qu'après que les données aient été récupérées à nouveau et affichées sur la page. Simple, élégant et efficace.",[56,822,58,823,58,828],{},[60,824,63,825,58],{"autoPlay":62,"loop":62,"muted":62,"playsInline":62},[65,826],{"src":827,"type":68},"https:\u002F\u002Fimages.soubiran.dev\u002Fposts\u002Fusing-pinia-colada-in-modals-without-spoiling-the-ux\u002Fthe-solution.mp4",[70,829,830],{},"La modale se ferme après que les données soient mises à jour.",[48,832,834],{"id":833},"enfin","Enfin",[10,836,837],{},"Ce petit problème souligne deux points importants :",[516,839,840,843],{},[519,841,842],{},"Chaque détail impacte l'expérience utilisateur. Ces petites nuances peuvent créer une grande différence à la fin.",[519,844,845],{},"Lire la documentation, encore et encore, même si cela peut sembler impertinent au départ, peut aider à résoudre des problèmes comme je l'ai fait ici.",[10,847,848,849,851],{},"J'espère que cet article clarifie l'utilisation de ",[20,850,22],{}," dans des modales sans dégrader l'expérience utilisateur. Si vous avez des questions ou des suggestions, n'hésitez pas à laisser un commentaire ci-dessous.",[10,853,854,855],{},"Pour voir le code en action, vous pouvez le trouver sur ",[83,856,86],{"repo":85},[858,859,860],"style",{},"html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}",{"title":102,"searchDepth":139,"depth":139,"links":862},[863,864,865,866],{"id":50,"depth":139,"text":51},{"id":510,"depth":139,"text":511},{"id":539,"depth":139,"text":540},{"id":833,"depth":139,"text":834},"11e74f5a-eed4-4419-a931-1c659ebd7ff3","2025-06-07","Loin d'être évident mais essentiel pour offrir une bonne expérience utilisateur, vous comprendrez quand attendre les requêtes invalidées dans Pinia Colada.","md",{},"\u002Ffr\u002Fposts\u002Fusing-pinia-colada-in-modals-without-spoiling-the-ux","---\ncontentId: 11e74f5a-eed4-4419-a931-1c659ebd7ff3\ntitle: Pinia Colada, les modales et l'expérience utilisateur\ndescription: Loin d'être évident mais essentiel pour offrir une bonne expérience utilisateur, vous comprendrez quand attendre les requêtes invalidées dans Pinia Colada.\ndate: 2025-06-07\n---\n\nDepuis plusieurs mois, j'utilise [**Pinia Colada**](https:\u002F\u002Fpinia-colada.esm.dev\u002F). Je l'ai découvert en recherchant une solution élégante pour récupérer des données sur mon site personnel (celui que vous lisez actuellement). Depuis, je l'ai utilisé dans tous mes projets qui nécessitent des données d'une API. C'est facile à utiliser, ça fonctionne parfaitement et cela me permet de créer de meilleures expériences grâce à sa mise en cache intégrée, sa fonctionnalité de revalidation des données en arrière-plan, et la facilité de mettre en œuvre des optimistic updates.\n\n**Pinia Colada** est une couche de récupération de données pour [**Pinia**](https:\u002F\u002Fpinia.vuejs.org\u002F), le store pour **Vue.js**, rendant la gestion d'état asynchrone d'une simplicité enfantine. Vous devriez vraiment l'essayer. Je ne peux plus commencer un nouveau projet sans lui désormais.\n\nCependant, j'ai récemment rencontré une difficulté en utilisant **Pinia Colada** dans une modale. Laissez-moi vous expliquer le problème et comment je l'ai résolu.\n\n## Le Problème\n\nAvant de me plonger dans l'explication, regardez la vidéo suivante :\n\n\u003Cfigure>\n  \u003Cvideo autoplay loop muted playsinline>\n    \u003Csource src=\"https:\u002F\u002Fimages.soubiran.dev\u002Fposts\u002Fusing-pinia-colada-in-modals-without-spoiling-the-ux\u002Fthe-problem.mp4\" type=\"video\u002Fmp4\">\n  \u003C\u002Fvideo>\n  \u003Cfigcaption>La modale se ferme avant que les données ne soient mises à jour.\u003C\u002Ffigcaption>\n\u003C\u002Ffigure>\n\nVous voyez le problème ?\n\nLorsque je soumets le formulaire dans la modale, le bouton \"Soumettre\" est désactivé et un indicateur de chargement apparaît. Jusqu'ici, tout va bien. Mais ensuite, la modale se ferme avant que les données ne soient mises à jour sur la page. L'utilisateur doit attendre, mais aucun retour ne s'affiche, ce qui nuit à l'expérience utilisateur. L'utilisateur ne sait pas si l'action a été réussie.\n\nVous pouvez trouver le code de cette vidéo sur \u003CGitHubLink repo=\"barbapapazes\u002Fpinia-colada-await-invalidate-queries\" \u002F>.\n\nPlongeons dans les détails techniques du problème et ses causes.\n\nLe code **Pinia Colada** que j'ai écrit pour muter les données est le suivant :\n\n```ts\nconst open = ref(false)\nconst { mutate: createComment, isLoading: isCreatingComment } = useMutation({\n  mutation: () => $fetch('\u002Fapi\u002Fcomments', {\n    method: 'POST',\n    body: comment.value\n  }),\n  onSettled: () => {\n    queryCache.invalidateQueries({ key: ['comments'] })\n  },\n  onSuccess: () => {\n    comment.value.text = ''\n    open.value = false\n  }\n})\n```\n\nLa variable `open` contrôle la visibilité de la modale.\n\nL'utilisateur ouvre la modale, remplit le formulaire et clique sur le bouton \"Soumettre\". La mutation `createComment` est alors appelée, envoyant des données à l'API via la fonction `mutate`. En cas de succès, la fonction `onSuccess` réinitialise le formulaire et ferme la modale. Enfin, la fonction `onSettled` invalide la requête `comments` pour récupérer à nouveau les données de l'API.\n\nLa requête **Pinia Colada** ressemble à ceci :\n\n```ts\nconst { state } = useQuery({\n  key: ['comments'],\n  query: () => $fetch('\u002Fapi\u002Fcomments')\n})\n```\n\nLe problème est que `onSettled`, qui invalide la requête et récupère à nouveau les données, est appelé après `onSuccess`, ce qui ferme la modale. Par conséquent, l'utilisateur voit la modale se fermer avant que les données n'apparaissent sur la page, ce qui crée de la confusion. \"Où est mon commentaire ? Ça a fonctionné ?\"\n\nSerait-il possible de déplacer `open.value = false` vers `onSettled`, après l'invalidation de la requête ? Malheureusement, non.\n\n## Solutions Multiples\n\nPour résoudre le problème, deux solutions me sont venues à l'esprit :\n\n- **Mises à Jour Optimistes** : C'est la meilleure solution pour l'expérience utilisateur, mettant immédiatement à jour l'interface, mais cela nécessite un code plus complexe. Ce n'est pas toujours faisable, surtout si l'API renvoie des données différentes de celles qui ont été envoyées. Par exemple, avec un système de commentaire permettant le markdown, l'API renvoie le markdown formaté, mais l'utilisateur envoie du markdown brut. Dans de tels cas, les mises à jour optimistes ne sont pas viables, et un état de chargement est la seule solution.\n- **État de Chargement** : C'est la solution la plus simple. Désactiver le bouton \"Soumettre\", afficher un indicateur de chargement et attendre la réponse de l'API. C'est la plus commune et la plus facile à mettre en œuvre. Cependant, dans une modale, cela nécessite des ajustements par rapport à un formulaire mis en ligne dans une page.\n\nPour mon projet, les mises à jour optimistes n'étaient pas une option, j'ai donc dû utiliser l'état de chargement. Pourtant, lors de sa mise en œuvre, j'ai rencontré ce problème. J'ai beau avoir lu la documentation **Pinia Colada** plusieurs fois, je n'avais pas de solution. À un moment, une idée a fini par germer. J'ai sauté dans la documentation pour voir si ma compréhension pouvait être une solution viable.\n\n## Attendre les Requêtes Invalidées\n\nLa documentation comprend une section sur [\"To `await` or not to `await`\"](https:\u002F\u002Fpinia-colada.esm.dev\u002Fguide\u002Fquery-invalidation.html#To-await-or-not-to-await) qui indique :\n\n> In mutations, it's possible to await within the different hooks. This will effectively delay the resolution or rejection of the mutation and its asyncStatus.\n\nC'était la clé. Je n'avais jamais compris le cas d'usage de l'attente des requêtes invalidées, mais c'est devenu clair avec ce problème. En attendant les requêtes invalidées, je pouvais retarder la fermeture de la modale jusqu'à ce que les données soient récupérées et disponibles sur la page.\n\nVoici le code pour mettre en œuvre cette solution :\n\n```ts\nconst open = ref(false)\nconst { mutate: createComment, isLoading: isCreatingComment } = useMutation({\n  mutation: () => $fetch('\u002Fapi\u002Fcomments', {\n    method: 'POST',\n    body: comment.value\n  }),\n  onSettled: async (_, error) => {\n    await queryCache.invalidateQueries({ key: ['comments'] })\n\n    if (!error) {\n      comment.value.text = ''\n      open.value = false\n    }\n  },\n})\n```\n\nAu lieu d'utiliser `onSuccess` pour fermer la modale, je l'ai déplacé vers `onSettled`, ajoutant un `await` avant l'invalidation du cache. Ainsi, la modale ne se ferme qu'après que les données aient été récupérées à nouveau et affichées sur la page. Simple, élégant et efficace.\n\n\u003Cfigure>\n  \u003Cvideo autoplay loop muted playsinline>\n    \u003Csource src=\"https:\u002F\u002Fimages.soubiran.dev\u002Fposts\u002Fusing-pinia-colada-in-modals-without-spoiling-the-ux\u002Fthe-solution.mp4\" type=\"video\u002Fmp4\">\n  \u003C\u002Fvideo>\n  \u003Cfigcaption>La modale se ferme après que les données soient mises à jour.\u003C\u002Ffigcaption>\n\u003C\u002Ffigure>\n\n## Enfin\n\nCe petit problème souligne deux points importants :\n\n- Chaque détail impacte l'expérience utilisateur. Ces petites nuances peuvent créer une grande différence à la fin.\n- Lire la documentation, encore et encore, même si cela peut sembler impertinent au départ, peut aider à résoudre des problèmes comme je l'ai fait ici.\n\nJ'espère que cet article clarifie l'utilisation de **Pinia Colada** dans des modales sans dégrader l'expérience utilisateur. Si vous avez des questions ou des suggestions, n'hésitez pas à laisser un commentaire ci-dessous.\n\nPour voir le code en action, vous pouvez le trouver sur \u003CGitHubLink repo=\"barbapapazes\u002Fpinia-colada-await-invalidate-queries\" \u002F>.\n",null,{"title":5,"description":869},{"loc":872},"fr\u002Fposts\u002Fusing-pinia-colada-in-modals-without-spoiling-the-ux","K1ODev9Xyqz0RGMHPX5UqJa72vtnLGYDXt1pd1jfzOo",1790086903578]