Using Pinia Colada in Modals Without Spoiling the UX
Since months, I've been using 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.
Pinia Colada is a data fetching layer for Pinia, 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.
However, I recently faced a challenge using Pinia Colada in a modal. Let me explain the problem and how I solved it.
The Problem
Before explaining, take a look at the following video:
Can you spot the problem?
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.
You can find the code for this video on pinia-colada-await-invalidate-queries.
With this high-level overview, let's delve into the technical details of the problem and its causes.
The Pinia Colada code I wrote to mutate the data is as follows:
const open = ref(false)
const { mutate: createComment, isLoading: isCreatingComment } = useMutation({
mutation: () => $fetch('/api/comments', {
method: 'POST',
body: comment.value
}),
onSettled: () => {
queryCache.invalidateQueries({ key: ['comments'] })
},
onSuccess: () => {
comment.value.text = ''
open.value = false
}
})
The open
variable controls the modal's visibility.
The 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.
The Pinia Colada query looks like this:
const { state } = useQuery({
key: ['comments'],
query: () => $fetch('/api/comments')
})
The 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?"
Could we simply move open.value = false
to onSettled
, after the query invalidation? Unfortunately, no.
Multiple Solutions
To solve the problem, two solutions came to mind:
- 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.
- 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.
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 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.
Awaiting for Invalidated Queries
The documentation includes a section about "To await
or not to await
" that states:
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.
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.
Here's the code to implement this solution:
const open = ref(false)
const { mutate: createComment, isLoading: isCreatingComment } = useMutation({
mutation: () => $fetch('/api/comments', {
method: 'POST',
body: comment.value
}),
onSettled: async (_, error) => {
await queryCache.invalidateQueries({ key: ['comments'] })
if (!error) {
comment.value.text = ''
open.value = false
}
},
})
Rather 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.
Finally
This small issue underscores two significant points:
- Every detail impacts user experience. Small nuances can create substantial differences.
- Read the documentation, again and again, even if it can seem irrelevant initially, can help solve problems as I did here.
I 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.
To see the code in action, you can find it on pinia-colada-await-invalidate-queries.
Thanks for reading! My name is Estéban, and I love to write about web development.
I've been coding for several years now, and I'm still learning new things every day. I enjoy sharing my knowledge with others, as I would have appreciated having access to such clear and complete resources when I first started learning programming.
If you have any questions or want to chat, feel free to comment below or reach out to me on Bluesky, X, and LinkedIn.
I hope you enjoyed this article and learned something new. Please consider sharing it with your friends or on social media, and feel free to leave a comment or a reaction below—it would mean a lot to me! If you'd like to support my work, you can sponsor me on GitHub!
Discussions
Add a Comment
You need to be logged in to access this feature.