Skip to content

Server state libraries

This chapter is a deep dive. You don't need it for the exam, and the running case app gets by without the library. It shows how larger apps manage data from the server, though, and in a project with a lot of data from the server it saves a good deal of code.

Learning goals

After this chapter you can

  • explain what server state is and how it differs from the rest of the state,
  • load and cache data with useQuery from Pinia Colada,
  • choose a key that fits the data,
  • change data with useMutation and reload stale queries,
  • place Pinia Colada and TanStack Query.

The data from the server is only a copy in the browser. The documentation of TanStack Query describes such server state with four properties. It lives in a place the app doesn't own, the app only reaches it through asynchronous calls, others can change it without the app knowing, and the app's copy can go out of date.

A store like the one in chapter 7.3 handles all of that itself. It loads, keeps the status, loads again after a change and doesn't know whether another page is loading the same data right now. Libraries for server state take over this work and keep the answers in a cache.

Pinia Colada calls itself the data fetching layer for Vue. It builds on Pinia and keeps its cache in a store. After npm install @pinia/colada, main.js installs it after Pinia.

main.js
const app = createApp(App)
app.use(createPinia())
// Pinia Colada keeps its cache in Pinia, so it comes after it
app.use(PiniaColada)
app.use(router)

useQuery gets a key and a function that fetches the data. Pinia Colada puts the answer into the cache under the key. According to the documentation, all queries with the same key share the same entry, and simultaneous requests only reach the server once.

CatalogView.vue
// The answer stays in the cache under the key, a second visit shows it right away
const { data: items, error, isPending } = useQuery({
key: ['items'],
query: ({ signal }) => getJson('/items', { signal }),
})

Among other things, useQuery returns data, error and isPending. According to the source code, data counts as fresh for five seconds. After that it is stale, and Pinia Colada loads it again when a component needs it again, when the window gets the focus back or when the network comes back. Entries that no component needs anymore are cleared after five minutes. staleTime and gcTime change both. The function gets a signal, with which Pinia Colada aborts a request that has been overtaken.

When the data depends on a value, such as the number of a thing, the value belongs in the key. Written as a function, the key follows the value, and every thing gets an entry of its own. A thing that was there before appears right away from the cache.

ItemView.vue
// A key per thing, a thing seen before comes from the cache
const { data: item } = useQuery({
key: () => ['items', props.id],
query: ({ signal }) => getJson(`/items/${props.id}`, { signal }),
})

Whatever changes data on the server goes through useMutation. Afterwards the affected queries are stale, in the example the own requests. invalidateQueries from useQueryCache marks them as stale and loads the queries again that a component is showing right now.

ItemView.vue
const form = reactive({ from: '', to: '' })
const queryCache = useQueryCache()
// After sending, the own requests are out of date, the cache loads them again
const { mutate: send, error: sendError } = useMutation({
mutation: (request) => postJson('/reservations', request),
onSettled: () => queryCache.invalidateQueries({ key: ['reservations'] }),
})

The other widely used library for server state is TanStack Query, formerly Vue Query, with packages for Vue, React and other frameworks. For Vue the package is called @tanstack/vue-query. The ideas are the same, and useQuery takes queryKey and queryFn there.

const { data, error, isPending } = useQuery({
queryKey: ['items'],
queryFn: () => getJson('/items'),
})

According to the documentation, data counts as stale right away in TanStack Query, and a failing query tries three times before it shows the error. For a project that already uses Pinia, Pinia Colada is the natural choice. People who also work with React know TanStack Query.

The exercises are in the project exercises/07-state/server-state/en in the course repo. The tests need no server. For the app in the browser, start the course API and sign in with the button in the header.

Terminal window
npm install
npm test

Exercise Getting started

CatalogView loads the catalog by hand and asks the course API on every visit. Load it with useQuery under the key ['items'] and show loading, error and the list.

Solution
CatalogView.vue
<script setup>
import { useQuery } from '@pinia/colada'
import { RouterLink } from 'vue-router'
import { getJson } from '@/api.js'
// The answer stays in the cache under the key, a second visit shows it right away
const { data: items, error, isPending } = useQuery({
key: ['items'],
query: ({ signal }) => getJson('/items', { signal }),
})
</script>
<template>
<h1>Catalog</h1>
<p v-if="isPending" role="status">Loading the catalog …</p>
<p v-else-if="error" class="error" role="alert">{{ error.message }}</p>
<ul v-else class="catalog">
<li v-for="item in items" :key="item.id">
<RouterLink :to="{ name: 'item', params: { id: item.id } }">{{ item.name }}</RouterLink>
<span class="category">{{ item.category }}</span>
</li>
</ul>
</template>

When someone comes back to the catalog within five seconds, Pinia Colada doesn't ask the API again. The test counts the requests to /items.

Exercise Going further

ItemView loads the thing again on every change. Load it with useQuery and a key made of 'items' and the number.

Solution
ItemView.vue
// A key per thing, a thing seen before comes from the cache
const { data: item } = useQuery({
key: () => ['items', props.id],
query: ({ signal }) => getJson(`/items/${props.id}`, { signal }),
})

Without the function, the key would stay with the first thing, and after a change the page would still show the old thing.

Exercise Challenge

Send the request with useMutation and then load the own requests under the key ['reservations'] again, so that the list on the page shows the new request.

Solution
ItemView.vue
const { data: requests } = useQuery({
key: ['reservations'],
query: ({ signal }) => getJson('/reservations', { signal }),
})
const form = reactive({ from: '', to: '' })
const queryCache = useQueryCache()
// After sending, the own requests are out of date, the cache loads them again
const { mutate: send, error: sendError } = useMutation({
mutation: (request) => postJson('/reservations', request),
onSettled: () => queryCache.invalidateQueries({ key: ['reservations'] }),
})
function submit() {
send({ itemId: props.id, ...form })
}

onSettled runs after success and after an error. A refused request can also show that the list is stale, for example when someone else has already requested the same days.

Self-check

  1. How does server state differ from the rest of an app's state?
  2. What does useQuery need a key for?
  3. When does Pinia Colada load data again by itself?
  4. What does invalidateQueries do after a change?
Answers
  1. It belongs to the server, others change it without the app knowing, and the copy in the browser can go out of date.
  2. The answer sits in the cache under the key. Queries with the same key share the entry and a running request.
  3. When the data is stale and a component needs it again, when the window gets the focus back or when the network comes back.
  4. It marks the queries with the key as stale and loads the ones again that a component is showing right now.

Text and figures are licensed under CC BY-SA 4.0, the code under the MIT License and the course data under CC0 1.0. The figures use the font Kalam under the SIL Open Font License 1.1. The sources are on GitHub.