vue

Data Fetching using Vue Hooks

darrenjennings

Darren Jennings

Posted on February 11, 2020

Data Fetching using Vue Hooks

Canonical URL: https://guuu.io/2020/data-fetching-vue-composition-api/

I discuss data fetching in Vue - where it stands today, and how a library like swrv can solve some common problems utilizing stale-while-revalidate caching.

Data fetching in an app can be a bit convoluted. If you're making XHR requests, you might see low latency from your high speed network connection or your low latency local network. You can emulate network conditions, but optimizing for speed can be an afterthought. While you want to empathize with your user base, implementing a frontend caching layer to speedup data fetches is usually low priority, especially if you reason that most of your users have high-speed connections / powerful devices.

Fetching Data in Vue

Traditionally in Vue, you might fetch your data in the mounted hook, sometimes called "render then fetch"

<template>
  <div :key="user.username" v-for="user in users">
    {{ user.username }}
  </div>
</template>

<script>
export default {
  name: 'Users',
  data() {
    return {
      users: []
    }
  },
  mounted() {
    fetch('/api/users')
      .then(res => res.json())
      .then(myJson => {
        this.users = myJson
      })
  }
}
</script>

There are many options now in how a developer might go about
fetching data, so even if you are triggering a fetch at different points in the render
lifecycle, your application typically will be constrained by the eventual inconsitent and cascading network latency.

For example if we also want to show all users with a link to their profile, e.g. a <UserProfile> component, the profile page will then need to fetch data from both the user and the user profile endpoints.

vue data fetching for a user profile component
<UserProfile> component being loaded

This is common in RESTful endpoints when an endpoint doesn't support
eager relations, specify join
fields, or if you are not using GraphQL
which is able to specify multiple return entities. The subsequent mounting and
network cost blocking the render could get expensive.

<template>
  <div v-if="profile">
    <img class="avatar" :src="profile.avatar" />
    <div>{{ profile.user.username }}</div>
    <div>{{ profile.twitter }}</div>
  </div>
  <div v-else>
    <Loading />
  </div>
</template>

<script>
export default {
  name: 'UserProfile',
  props: {
    username: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      profile: null
    }
  },

  mounted() {
    fetch(`/api/user/${this.username}`)
      .then(res => {
        return res.json()
      })
      .then(user => {
        fetch(`/api/user/${user.id}/profile`)
          .then(res => {
            return res.json()
          })
          .then(profile => {
            this.profile = {
              ...profile,
              user
            }
          })
      })
  }
}
</script>

This gets a bit ugly and is not reactive in the event that username changes. Let's clean it up a bit with the @vue/composition-api to keep the data flowing and utilize the new vue setup function.

<template>
  <div v-if="profile">
    <img class="avatar" :src="profile.avatar" />
    <div>{{ profile.user.username }}</div>
    <div>{{ profile.twitter }}</div>
  </div>
  <div v-else>
    <Loading />
  </div>
</template>

<script>
import { ref, watch } from '@vue/composition-api'

export default {
  name: 'UserProfile',
  props: {
    username: {
      type: String,
      required: true
    }
  },
  setup(props) {
    const profile = ref([])

    // when props.username changes, vue's reactivity automatically
    // registers the change and re-triggers the data fetch.
    watch(() => {
      fetch(`/api/user/${props.username}`)
        .then(res => res.json())
        .then(user => {
          fetch(`/api/user/${user.id}/profile`)
            .then(res => res.json())
            .then(profile => {
              profile.value = { ...profile, user }
            })
        })
    })

    return {
      profile
    }
  })
}
</script>

Now that we have the data flowing, there is still a cost to rendering this component. If a user navigates away, then returns, the component will fetch the data all over again, even though the user just saw the data! This becomes frustrating for end users who are at the mercy of their network speeds.

Caching the Response

Have you ever clicked a "back" link in a web application and the data that you just saw is now taking an obscene amount of time to refetch? The browser can sometimes help with cache headers, and browser history cache, but in modern applications the server side rendered portion of our pages are only a segment of a user's navigation lifecycle. We need a multi-pronged caching strategy so that our dynamic pages, with client-side fetching, can be consistently fast and always online.[1]

Stale-while-revalidate

Stale-while-revalidate (SWR) is a cache invalidation strategy popularized by HTTP RFC 5861.

If a cached response is served stale due to the presence of this extension, the cache SHOULD attempt to revalidate it while still serving stale responses (i.e., without blocking).

The key here is "not blocking". In our earlier example, the <UserProfile> component would fetch from /api/users, then /api/users/:id/profile, always assuming that new fetches were fresh. This is a good thing, since users always want to see the latest data. However, this blocked the rendering of data until a response was received, even if a user had seen the response recently.

A SWR caching strategy would allow users to see stale data first while fetching, giving an eventually consistent UI.

stale-while-revalidate data fetching vue

Profile immediately returns from cache on the left, then once the fetch finishes,
then the new image/twitter handle is updated.

In the component, you might want to cache a response in a global store or localStorage. Here's our component might work:

import { ref, watch } from '@vue/composition-api'

export default {
  name: 'UserProfile',
  props: {
    username: {
      type: String,
      required: true
    }
  },
  setup(props, { root }) {
    const profile = ref([])

    function getFromCache(key) {
      return root.$store.getters['cache/getCacheItem'](key)
    }

    const cacheKey = `${props.username}-profile`

    watch(() => {
      // get STALE content and set data
      profile.value = getFromCache(cacheKey)

      // WHILE-REVALIDATE and go fetch the data anyways,
      // producing immediately cached results, with an
      // eventually consistent UI.
      fetch(`/api/user/${props.username}`)
        .then(res => res.json())
        .then(user => {
          fetch(`/api/user/${user.id}/profile`)
            .then(res =>  res.json())
            .then(profile => {
              profile.value = {
                ...profile,
                user
              }
              root.$store.dispatch('cache/setCacheItem', {
                key: cacheKey,
                profile
              })
            })
          })
        })

      return {
        profile
      }
    })
  }
}

This helps us get the strategy correct, but we'd like to have a library do this for us, so that we can have a simpler api while continuing to add new features: in-flight de-duplication, library agnostic fetching, error handling, loading states, different caching strategies, polling, onFocus revalidation etc.

Introducing swrv

swrv is a library using @vue/composition-api hooks for remote data fetching. It is largely a port of swr. Our example can be refactored:

import fetcher from './fetcher'
import useSWRV from 'swrv'

export default {
  name: 'UserProfile',
  props: {
    username: {
      type: String,
      required: true
    }
  },

  setup(props) {
    const { data: user, error: error1 } = useSWRV(
      `/api/users/${props.username}`,
      fetcher
    )
    const { data: profile, error: error2 } = useSWRV(
      () => `/api/users/${user.value.id}/profile`,
      fetcher
    )

    return {
      profile
    }
  }
}

In this example, the Vue Hook useSWRV accepts a key and a fetcher function. key is a unique identifier of the request, and here it is the URL of the API. The fetcher accepts the key as its parameter and returns the data asynchronously. The key can also be a function, with its own dependencies. The second useSWRV hook actually has a dependency on the response from the first hook. swrv will handle this by watching dependencies inside the key function and re-validate when these change. This is helpful for avoiding unnecessary fetches, and also reacting to key changes, in the event you'd like to pass in query params such as pagination/filtering.

useSWRV here returns 2 values: data and error. When the request (fetcher) is not yet finished, data will be undefined. And when we get a response, it sets data and error based on the result of fetcher and rerenders the component. This is because data and error are Vue
Refs, and their values will be set by the fetcher response. The fetcher function can be any asynchronous function, so you can use your favorite data-fetching library.

Features

swrv handles some of the more complex feature sets, such as:

  • in-flight promise de-duplication in the event that a page loads the same data in multiple components.


Deduplication

  • focus and page availability revalidation events for when a user switches tabs or clicks away during the browser session. This helps with an app feeling consistently up to date, or "alive", all while being customizable to what the data source requires. e.g. expensive requests might want to limit as many fetches as possible.
  • interval polling, to check if data has been updated, all while still serving from cache, and stops polling if user is offline or the window is not active.
  • prefetching, or "warming" the cache - useful for when you anticipate a user's actions like hovering over a link or preloading common page data.
  • custom caching strategies - by default cache is in-memory, but can be customized to use localStorage for better offline experiences. swrv manages the cache store, and provides ttl mechanisms for eviction.
  • pagination. here is a short demo of using pagination in swrv

For more information on swrv and it's features, check out the
Github Repository.

1. Guillermo Rauch - 2019 in Review

💖 💪 🙅 🚩
darrenjennings
Darren Jennings

Posted on February 11, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related