[{"data":1,"prerenderedAt":877},["ShallowReactive",2],{"post-\u002Fposts\u002Fusing-pinia-colada-in-modals-without-spoiling-the-ux":3},{"id":4,"title":5,"body":6,"contentId":865,"date":866,"description":867,"extension":868,"meta":869,"navigation":62,"path":870,"rawbody":871,"robots":872,"schemaOrg":872,"seo":873,"sitemap":874,"stem":875,"__hash__":876},"posts\u002Fposts\u002Fusing-pinia-colada-in-modals-without-spoiling-the-ux.md","Using Pinia Colada in Modals Without Spoiling the UX",{"type":7,"value":8,"toc":859},"minimark",[9,24,41,47,52,55,73,76,79,87,90,96,380,386,408,413,487,496,506,510,513,529,535,539,555,561,564,567,807,819,829,833,836,844,850,855],[10,11,12,13,23],"p",{},"Since months, I've been using ",[14,15,19],"a",{"href":16,"rel":17},"https:\u002F\u002Fpinia-colada.esm.dev\u002F",[18],"nofollow",[20,21,22],"strong",{},"Pinia Colada",". I discovered it while searching for an elegant solution for data fetching on my personal website (the one you're reading right now). Since then, I've used it in all my projects that require data from an API. It's easy to use, functions seamlessly, and enables me to create better experiences with its built-in cache, stale-while-revalidate feature, and the ease of implementing optimistic updates.",[10,25,26,28,29,36,37,40],{},[20,27,22],{}," is a data fetching layer for ",[14,30,33],{"href":31,"rel":32},"https:\u002F\u002Fpinia.vuejs.org\u002F",[18],[20,34,35],{},"Pinia",", the intuitive store for ",[20,38,39],{},"Vue.js",", making asynchronous state management a breeze. You should definitely give it a try. I can't start a new project without it anymore.",[10,42,43,44,46],{},"However, I recently faced a challenge using ",[20,45,22],{}," in a modal. Let me explain the problem and how I solved it.",[48,49,51],"h2",{"id":50},"the-problem","The problem",[10,53,54],{},"Before explaining, take a look at the following video:",[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",{},"The modal closes before the data is updated.",[10,74,75],{},"Can you spot the problem?",[10,77,78],{},"When I submit the form in the modal, the \"Submit\" button is disabled, and a loading spinner appears. So far, so good. But then, the modal closes before the data updates on the page. The user has to wait, yet no feedback is provided, resulting in a terrible user experience. The user remains unaware whether the action was successful.",[10,80,81,82],{},"You can find the code for this video on ",[83,84,86],"git-hub-link",{"repo":85},"barbapapazes\u002Fpinia-colada-await-invalidate-queries",".",[10,88,89],{},"With this high-level overview, let's delve into the technical details of the problem and its causes.",[10,91,92,93,95],{},"The ",[20,94,22],{}," code I wrote to mutate the data is as follows:",[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,92,382,385],{},[104,383,384],{},"open"," variable controls the modal's visibility.",[10,387,388,389,392,393,396,397,400,401,404,405,407],{},"The user opens the modal, fills the form, and clicks the \"Submit\" button. The ",[104,390,391],{},"createComment"," mutation is then called, sending data to the API via the ",[104,394,395],{},"mutate"," function. Upon success, the ",[104,398,399],{},"onSuccess"," callback resets the form and closes the modal. Finally, the ",[104,402,403],{},"onSettled"," callback invalidates the ",[104,406,301],{}," query to refetch data from the API.",[10,409,92,410,412],{},[20,411,22],{}," query looks like this:",[97,414,416],{"className":99,"code":415,"language":101,"meta":102,"style":102},"const { state } = useQuery({\n  key: ['comments'],\n  query: () => $fetch('\u002Fapi\u002Fcomments')\n})\n",[104,417,418,438,458,481],{"__ignoreMap":102},[107,419,420,422,424,427,429,431,434,436],{"class":109,"line":110},[107,421,114],{"class":113},[107,423,144],{"class":121},[107,425,426],{"class":117}," state ",[107,428,168],{"class":121},[107,430,171],{"class":121},[107,432,433],{"class":125}," useQuery",[107,435,129],{"class":117},[107,437,179],{"class":121},[107,439,440,443,445,447,449,451,453,456],{"class":109,"line":139},[107,441,442],{"class":147},"  key",[107,444,151],{"class":121},[107,446,296],{"class":117},[107,448,201],{"class":121},[107,450,301],{"class":204},[107,452,201],{"class":121},[107,454,455],{"class":117},"]",[107,457,231],{"class":121},[107,459,460,463,465,467,469,471,473,475,477,479],{"class":109,"line":182},[107,461,462],{"class":125},"  query",[107,464,151],{"class":121},[107,466,190],{"class":121},[107,468,193],{"class":113},[107,470,196],{"class":125},[107,472,129],{"class":117},[107,474,201],{"class":121},[107,476,205],{"class":204},[107,478,201],{"class":121},[107,480,136],{"class":117},[107,482,483,485],{"class":109,"line":215},[107,484,168],{"class":121},[107,486,136],{"class":117},[10,488,489,490,492,493,495],{},"The issue is that ",[104,491,403],{},", which invalidates the query and re-fetches the data, is called after ",[104,494,399],{},", which closes the modal. Consequently, the user can see the modal closing before the data appears on the page, causing confusion. \"Where is my comment? Did it work?\"",[10,497,498,499,502,503,505],{},"Could we simply move ",[104,500,501],{},"open.value = false"," to ",[104,504,403],{},", after the query invalidation? Unfortunately, no.",[48,507,509],{"id":508},"multiple-solutions","Multiple solutions",[10,511,512],{},"To solve the problem, two solutions came to mind:",[514,515,516,523],"ul",{},[517,518,519,522],"li",{},[20,520,521],{},"Optimistic Updates",": This is the best solution for user experience, immediately updating the UI, but results in more complex code. It's not always feasible, especially if the API returns data different from what was sent. For example, with a comment system allowing markdown, the API returns formatted markdown, but the user sends raw markdown. In such cases, optimistic updates aren't viable, and a loading state is the only solution.",[517,524,525,528],{},[20,526,527],{},"Loading State",": This is the simplest solution. Disable the \"Submit\" button, display a loading spinner, and wait for the API response. It's the most common and easiest to implement. However, in a modal, it requires adjustments compared to an inline form.",[10,530,531,532,534],{},"For my project, optimistic updates weren't an option, so I had to use a loading state. Yet while implementing it, I faced this problem. No matter how many times I read the ",[20,533,22],{}," documentation, I couldn't come up with a solution. At some point, I came up with an idea. I jumped to the documentation to check if my understanding could be a viable solution.",[48,536,538],{"id":537},"awaiting-for-invalidated-queries","Awaiting for invalidated queries",[10,540,541,542,554],{},"The documentation includes a section about ",[14,543,546,547,550,551,553],{"href":544,"rel":545},"https:\u002F\u002Fpinia-colada.esm.dev\u002Fguide\u002Fquery-invalidation.html#To-await-or-not-to-await",[18],"\"To ",[104,548,549],{},"await"," or not to ",[104,552,549],{},"\""," that states:",[556,557,558],"blockquote",{},[10,559,560],{},"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,562,563],{},"This was the key. I had never understood the case for awaiting invalidated queries, but it became clear with this problem. By awaiting the invalidated queries, I could delay closing the modal until the data was re-fetched and available on the page.",[10,565,566],{},"Here's the code to implement this solution:",[97,568,570],{"className":99,"code":569,"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,571,572,588,618,642,656,668,676,703,738,743,761,778,791,796,800],{"__ignoreMap":102},[107,573,574,576,578,580,582,584,586],{"class":109,"line":110},[107,575,114],{"class":113},[107,577,118],{"class":117},[107,579,122],{"class":121},[107,581,126],{"class":125},[107,583,129],{"class":117},[107,585,133],{"class":132},[107,587,136],{"class":117},[107,589,590,592,594,596,598,600,602,604,606,608,610,612,614,616],{"class":109,"line":139},[107,591,114],{"class":113},[107,593,144],{"class":121},[107,595,148],{"class":147},[107,597,151],{"class":121},[107,599,154],{"class":117},[107,601,157],{"class":121},[107,603,160],{"class":147},[107,605,151],{"class":121},[107,607,165],{"class":117},[107,609,168],{"class":121},[107,611,171],{"class":121},[107,613,174],{"class":125},[107,615,129],{"class":117},[107,617,179],{"class":121},[107,619,620,622,624,626,628,630,632,634,636,638,640],{"class":109,"line":182},[107,621,185],{"class":125},[107,623,151],{"class":121},[107,625,190],{"class":121},[107,627,193],{"class":113},[107,629,196],{"class":125},[107,631,129],{"class":117},[107,633,201],{"class":121},[107,635,205],{"class":204},[107,637,201],{"class":121},[107,639,157],{"class":121},[107,641,212],{"class":121},[107,643,644,646,648,650,652,654],{"class":109,"line":215},[107,645,218],{"class":147},[107,647,151],{"class":121},[107,649,223],{"class":121},[107,651,226],{"class":204},[107,653,201],{"class":121},[107,655,231],{"class":121},[107,657,658,660,662,664,666],{"class":109,"line":234},[107,659,237],{"class":147},[107,661,151],{"class":121},[107,663,242],{"class":117},[107,665,86],{"class":121},[107,667,247],{"class":117},[107,669,670,672,674],{"class":109,"line":250},[107,671,253],{"class":121},[107,673,256],{"class":117},[107,675,231],{"class":121},[107,677,678,680,682,685,688,692,694,697,699,701],{"class":109,"line":261},[107,679,264],{"class":125},[107,681,151],{"class":121},[107,683,684],{"class":113}," async",[107,686,687],{"class":121}," (",[107,689,691],{"class":690},"sHdIc","_",[107,693,157],{"class":121},[107,695,696],{"class":690}," error",[107,698,256],{"class":121},[107,700,193],{"class":113},[107,702,212],{"class":121},[107,704,705,709,712,714,716,718,720,722,724,726,728,730,732,734,736],{"class":109,"line":275},[107,706,708],{"class":707},"s7zQu","    await",[107,710,711],{"class":117}," queryCache",[107,713,86],{"class":121},[107,715,283],{"class":125},[107,717,129],{"class":147},[107,719,288],{"class":121},[107,721,291],{"class":147},[107,723,151],{"class":121},[107,725,296],{"class":147},[107,727,201],{"class":121},[107,729,301],{"class":204},[107,731,201],{"class":121},[107,733,306],{"class":147},[107,735,168],{"class":121},[107,737,136],{"class":147},[107,739,740],{"class":109,"line":313},[107,741,742],{"emptyLinePlaceholder":62},"\n",[107,744,745,748,750,753,756,759],{"class":109,"line":319},[107,746,747],{"class":707},"    if",[107,749,687],{"class":147},[107,751,752],{"class":121},"!",[107,754,755],{"class":117},"error",[107,757,758],{"class":147},") ",[107,760,179],{"class":121},[107,762,763,766,768,770,772,774,776],{"class":109,"line":333},[107,764,765],{"class":117},"      comment",[107,767,86],{"class":121},[107,769,341],{"class":117},[107,771,86],{"class":121},[107,773,346],{"class":117},[107,775,171],{"class":121},[107,777,351],{"class":121},[107,779,780,783,785,787,789],{"class":109,"line":354},[107,781,782],{"class":117},"      open",[107,784,86],{"class":121},[107,786,341],{"class":117},[107,788,171],{"class":121},[107,790,366],{"class":132},[107,792,793],{"class":109,"line":369},[107,794,795],{"class":121},"    }\n",[107,797,798],{"class":109,"line":375},[107,799,316],{"class":121},[107,801,803,805],{"class":109,"line":802},15,[107,804,168],{"class":121},[107,806,136],{"class":117},[10,808,809,810,812,813,815,816,818],{},"Rather than using ",[104,811,399],{}," to close the modal, I moved it to ",[104,814,403],{},", adding an ",[104,817,549],{}," before the cache invalidation. Thus, the modal closes only after the data is re-fetched and displayed on the page. Simple, elegant, and effective.",[56,820,58,821,58,826],{},[60,822,63,823,58],{"autoPlay":62,"loop":62,"muted":62,"playsInline":62},[65,824],{"src":825,"type":68},"https:\u002F\u002Fimages.soubiran.dev\u002Fposts\u002Fusing-pinia-colada-in-modals-without-spoiling-the-ux\u002Fthe-solution.mp4",[70,827,828],{},"The modal closes after the data is updated.",[48,830,832],{"id":831},"finally","Finally",[10,834,835],{},"This small issue underscores two significant points:",[514,837,838,841],{},[517,839,840],{},"Every detail impacts user experience. Small nuances can create substantial differences.",[517,842,843],{},"Read the documentation, again and again, even if it can seem irrelevant initially, can help solve problems as I did here.",[10,845,846,847,849],{},"I hope this article clarifies this usage of ",[20,848,22],{}," in modals without degrading user experience. If you have questions or suggestions, feel free to leave a comment below.",[10,851,852,853],{},"To see the code in action, you can find it on ",[83,854,86],{"repo":85},[856,857,858],"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":860},[861,862,863,864],{"id":50,"depth":139,"text":51},{"id":508,"depth":139,"text":509},{"id":537,"depth":139,"text":538},{"id":831,"depth":139,"text":832},"9eb990da-ce9f-42ea-9836-5bb1fca28691","2025-06-07","Far from being straightforward but essential to provide a good user experience, you'll understand when to await for invalidated queries in Pinia Colada.","md",{},"\u002Fposts\u002Fusing-pinia-colada-in-modals-without-spoiling-the-ux","---\ncontentId: 9eb990da-ce9f-42ea-9836-5bb1fca28691\ntitle: Using Pinia Colada in Modals Without Spoiling the UX\ndescription: Far from being straightforward but essential to provide a good user experience, you'll understand when to await for invalidated queries in Pinia Colada.\ndate: 2025-06-07\n---\n\nSince months, I've been using [**Pinia Colada**](https:\u002F\u002Fpinia-colada.esm.dev\u002F). I discovered it while searching for an elegant solution for data fetching on my personal website (the one you're reading right now). Since then, I've used it in all my projects that require data from an API. It's easy to use, functions seamlessly, and enables me to create better experiences with its built-in cache, stale-while-revalidate feature, and the ease of implementing optimistic updates.\n\n**Pinia Colada** is a data fetching layer for [**Pinia**](https:\u002F\u002Fpinia.vuejs.org\u002F), the intuitive store for **Vue.js**, making asynchronous state management a breeze. You should definitely give it a try. I can't start a new project without it anymore.\n\nHowever, I recently faced a challenge using **Pinia Colada** in a modal. Let me explain the problem and how I solved it.\n\n## The problem\n\nBefore explaining, take a look at the following video:\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>The modal closes before the data is updated.\u003C\u002Ffigcaption>\n\u003C\u002Ffigure>\n\nCan you spot the problem?\n\nWhen I submit the form in the modal, the \"Submit\" button is disabled, and a loading spinner appears. So far, so good. But then, the modal closes before the data updates on the page. The user has to wait, yet no feedback is provided, resulting in a terrible user experience. The user remains unaware whether the action was successful.\n\nYou can find the code for this video on \u003CGitHubLink repo=\"barbapapazes\u002Fpinia-colada-await-invalidate-queries\" \u002F>.\n\nWith this high-level overview, let's delve into the technical details of the problem and its causes.\n\nThe **Pinia Colada** code I wrote to mutate the data is as follows:\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\nThe `open` variable controls the modal's visibility.\n\nThe user opens the modal, fills the form, and clicks the \"Submit\" button. The `createComment` mutation is then called, sending data to the API via the `mutate` function. Upon success, the `onSuccess` callback resets the form and closes the modal. Finally, the `onSettled` callback invalidates the `comments` query to refetch data from the API.\n\nThe **Pinia Colada** query looks like this:\n\n```ts\nconst { state } = useQuery({\n  key: ['comments'],\n  query: () => $fetch('\u002Fapi\u002Fcomments')\n})\n```\n\nThe issue is that `onSettled`, which invalidates the query and re-fetches the data, is called after `onSuccess`, which closes the modal. Consequently, the user can see the modal closing before the data appears on the page, causing confusion. \"Where is my comment? Did it work?\"\n\nCould we simply move `open.value = false` to `onSettled`, after the query invalidation? Unfortunately, no.\n\n## Multiple solutions\n\nTo solve the problem, two solutions came to mind:\n\n- **Optimistic Updates**: This is the best solution for user experience, immediately updating the UI, but results in more complex code. It's not always feasible, especially if the API returns data different from what was sent. For example, with a comment system allowing markdown, the API returns formatted markdown, but the user sends raw markdown. In such cases, optimistic updates aren't viable, and a loading state is the only solution.\n- **Loading State**: This is the simplest solution. Disable the \"Submit\" button, display a loading spinner, and wait for the API response. It's the most common and easiest to implement. However, in a modal, it requires adjustments compared to an inline form.\n\nFor my project, optimistic updates weren't an option, so I had to use a loading state. Yet while implementing it, I faced this problem. No matter how many times I read the **Pinia Colada** documentation, I couldn't come up with a solution. At some point, I came up with an idea. I jumped to the documentation to check if my understanding could be a viable solution.\n\n## Awaiting for invalidated queries\n\nThe documentation includes a section about [\"To `await` or not to `await`\"](https:\u002F\u002Fpinia-colada.esm.dev\u002Fguide\u002Fquery-invalidation.html#To-await-or-not-to-await) that states:\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\nThis was the key. I had never understood the case for awaiting invalidated queries, but it became clear with this problem. By awaiting the invalidated queries, I could delay closing the modal until the data was re-fetched and available on the page.\n\nHere's the code to implement this 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\nRather than using `onSuccess` to close the modal, I moved it to `onSettled`, adding an `await` before the cache invalidation. Thus, the modal closes only after the data is re-fetched and displayed on the page. Simple, elegant, and effective.\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>The modal closes after the data is updated.\u003C\u002Ffigcaption>\n\u003C\u002Ffigure>\n\n## Finally\n\nThis small issue underscores two significant points:\n\n- Every detail impacts user experience. Small nuances can create substantial differences.\n- Read the documentation, again and again, even if it can seem irrelevant initially, can help solve problems as I did here.\n\nI hope this article clarifies this usage of **Pinia Colada** in modals without degrading user experience. If you have questions or suggestions, feel free to leave a comment below.\n\nTo see the code in action, you can find it on \u003CGitHubLink repo=\"barbapapazes\u002Fpinia-colada-await-invalidate-queries\" \u002F>.\n",null,{"title":5,"description":867},{"loc":870},"posts\u002Fusing-pinia-colada-in-modals-without-spoiling-the-ux","4csWvRXbYWPSJl-UnWmyX1rvgcKe-wwjF7B_dHCeERA",1790086900817]