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:

The modal closes before the data is updated.

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:

ts
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:

ts
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:

ts
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.

The modal closes after the data is updated.

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.

PP

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!

Reactions

Discussions

Add a Comment

You need to be logged in to access this feature.

Support my work
Follow me on