When building applications with frontend frameworks, one aspect that generates a lot of opinions is the concept of data fetching. In basic scenarios, you would handle your data using a combination of an HTTP client (e.g Axios) and Vue’s lifecycle methods.
More complex situations such as larger applications with multiple components, which in most cases will be routed, will require a different approach. Also, the sequence in which data is to be fetched — before or after certain components have been loaded — is also a concern. In this post, we’ll take a look at the best practices for fetching data in advanced scenarios when working with Vue. We’ll also learn how we can use Vue 3’s composition API to leverage the stale-while-revalidate
technique in data fetching.
Data fetching when various routed components are involved could get complex. In some cases, we may want to display some data while a particular route is being navigated. Vue Router handles this properly by providing options to either fetch the data before navigation is made or after. Let’s take a look at both scenarios.
Vue Router provides a number of hooks to handle data fetching before navigation:
beforeRouteEnter()
hook which takes three arguments – to
, from
, and next
. Like the name suggests, it is used to fetch the data needed in a component before navigation is done to that componentbeforeRouteUpdate()
hook which also takes the same arguments as beforeRouteEnter()
, is used to update the component with the data which has been fetchedWe have an app with two routed components that display the current and ledger balances of our bank account. When displaying our ledger balance, we want to fetch the data before navigating to the component. First, we’ll create the script that provides the data for our ledger balance:
// LedgerBalance.js export default (callback) => { const LedgerBalance = "1500 USD"; setTimeout(() => { callback(null, LedgerBalance); }, 3000); };
Next, we’ll create our ledger balance component:
<!-- LedgerBalance.vue --> <template> <div>Hello, your ledger balance is {{ balance }}</div> </template> <script> import ledgerBalance from "../scripts/LedgerBalance"; export default { name: "Ledger", data() { return { balance: null }; }, // Here the component is yet to be loaded beforeRouteEnter(to, from, next) { ledgerBalance((err, balance) => { next(vm => vm.setBalance(err, balance)); }); }, // On calling beforeRouteUpdate, the component is loaded and the route changes beforeRouteUpdate(to, from, next) { this.balance = null; ledgerBalance((err, balance) => { this.setBalance(err, balance); next(); }); }, methods: { setBalance(err, balance) { if (err) { console.error(err); } else { this.balance = balance; } } } }; </script>
In LedgerBalance.vue
, the beforeRouteEnter()
hook ensures that data is fetched from LedgerBalance.js
before the Ledger
component is loaded.
Next when Ledger
is loaded and the route changes, the setBalance()
method within beforeRouteUpdate()
is then used to set the data.
Then we’ll define the route path for our ledger balance:
// main.js import Vue from "vue"; import VueRouter from "vue-router"; import App from "./App"; import Ledger from "./components/LedgerBalance"; Vue.use(VueRouter); const router = new VueRouter({ routes: [ { path: "/Ledger", component: Ledger } ] }); new Vue({ render: (h) => h(App), router }).$mount("#app");
After defining the route path, we’ll include it in the main view:
<!-- App.vue --> <template> <div id="app"> <div class="nav"> <router-link to="/Ledger">Ledger Balance</router-link> </div> <hr> <div class="router-view"> <router-view></router-view> </div> </div> </template> <script> export default { name: "App" }; </script>
When navigating to the Ledger
component, we can observe the delay (due to the setTimeout()
function in LedgerBalance.js
) as the data is fetched before navigation:
Screen Recording 2020 08 16 at 12 38 24
Uploaded by Raphael Ugwu on 2020-08-16.
In some cases, we may want to fetch our data after we’ve navigated to our component. This could be useful where we are working with data that changes in real-time. Such as the current balance of an account. In this instance, we would first define the function which handles the data for our current balance:
// CurrentBalance.js export default (callback) => { const CurrentBalance = "1000 USD"; setTimeout(() => { callback(null, CurrentBalance); }, 3000); };
Next, when creating our CurrentBalance
component, we’ll use the created()
lifecycle hook to call fetchBalance()
, our data fetching method:
/<!-- CurrentBalance.vue --> <template> <div>Hello, your current balance is {{ balance }}</div> </template> <script> import currentBalance from "../scripts/CurrentBalance"; export default { name: "Current", data() { return { balance: null }; }, created() { this.fetchBalance(); }, methods: { fetchBalance() { currentBalance((err, balance) => { if (err) { console.error(err); } else { this.balance = balance; } }); } } }; </script>
Taking into account that the data will be fetched after navigation occurs, a loading component such as a progress bar or a spinner would be useful so it doesn’t look like there’s been an error:
Screen Recording 2020 08 16 at 16 03 49
Uploaded by Raphael Ugwu on 2020-08-16.
Traditionally, Vue apps used the mounted()
hook to fetch data. To fetch data from an API would be something similar to the code sample below:
<template> <div :key="car.carmodel" v-for="car in cars"> {{ car.carmodel }} </div> </template> <script> export default { name: 'Cars', data() { return { cars: [] } }, mounted() { fetch('/api/cars') .then(res => res.json()) .then(carJson => { this.cars = carJson }) } } </script>
Suppose more data is required here, this could result in fetching data from multiple endpoints. This isn’t feasible as navigating between different components will result in making an API request for each navigation. These multiple API requests could slow things down and create a poor experience for users of this app:
<template> <div v-if="about"> <div>{{ about.car.model }}</div> <div>{{ about.bio }}</div> </div> <div v-else> <Loading /> </div> </template> <script> export default { name: 'Bio', props: { model: { type: String, required: true } }, data() { return { about: null } }, mounted() { fetch(`/api/car/${this.model}`) .then(res => res.json()) .then(car => fetch(`/api/car/${car.id}/about`)) .then(res => res.json()) .then(about => { this.about = { ...about, car } }) } } </script>
Ideally what’s needed is an efficient way of caching already visited components such that each component which has already been visited presents data readily, even when it is navigated away from and back. Here’s where the “stale-while-revalidate” concept is useful.
Stale-while-revalidate (swr)
enables us cached, already fetched data while we fetch additional requested data. In the above code sample, every request to view the car’s bio would also re-trigger the request to view the car’s model even if that has been seen. With swr
, we can cache the car’s model so when a user requests for the car’s bio, the model is seen while the data for the car’s bio is being fetched.
In Vue, the stale-while-revalidate concept is made possible through a library (swrv) which largely makes use of Vue’s composition API. Employing a more scalable approach, we can use swrv
to fetch details of our car like this:
import carInfo from './carInfo' import useSWRV from 'swrv' export default { name: 'Bio', props: { model: { type: String, required: true } }, setup(props) { const { data: car, error: carError } = useSWRV( `/api/cars/${props.model}`, carInfo ) const { data: about, error: aboutError } = useSWRV( () => `/api/cars/${car.value.id}/about`, carInfo ) return { about } } }
In the code sample above, the first useSWRV
hook uses the API’s URL as a unique identifier for each request made and also accepts a carInfo
function. carInfo
is an asynchronous function which is used to fetch details about the car, it also uses the API’s URL as a unique identifier.
The second useSWRV
hook watches for changes from the first hook and revalidates its data based on these changes. The data
and error
values are respectively populated for successful and failed responses to requests made by the carInfo
function.
Concepts to fetching data while navigating routed components and caching already fetched data are awesome ways to create an impressive user experience in your application if used properly. To get a deeper dive into stale-while-revalidate, I recommend you take a look at Markus Oberlehner’s post on the topic.
Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens in your Vue apps, including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.
The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error and what state the application was in when an issue occurred.
Modernize how you debug your Vue apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]