Data fetching is a recurrent theme in modern JavaScript app development. The fetching itself is simple enough, all we really need is the fetch
function or a third-party library like Axios. But in practice, we need to worry about more than just the fetching.
Whenever an app has to deal with remote data, we should be caching that data for performance purposes. And with caching, we need to be updating the cache at the right time. So, wouldn’t it be nice if we had a tool that combined fetching and cache management as one coherent concern?
There is a new wave of tools that employ a caching pattern called stale-while-revalidate, orSWR. Stale means that the data in the cache is outdated. Revalidate means to get a fresh copy of the data from the remote server to update the local cache.
Basically, stale-while-revalidate means whenever the app fetches data, the locally cached copy will be served to the app immediately, then a fetch will be made. And eventually, a fresh copy from the server will replace the cached copy, and in turn, updates the app.
This caching policy is the best of both worlds because it makes the UI feel snappy without any lag, and the data stays up-to-date.
In this tutorial, we’ll check out an SWR implementation for Vue.js called SWRV (V for Vue). This library is meant to be used with Vue’s composition API. So, that means this tutorial does require some familiarity with the composition API, specifically about the setup
function, ref
, and computed
property. (You can learn about all of these concepts in our Vue 3 Composition API course.)
Throughout this article, we’re going to create a Vue app and a JSON API, and then use SWRV to facilitate the data exchange between them.
New App
First, let’s create a Vue 3 app using the Vue CLI:
vue create swr-app
And choose Vue 3 in the prompt:
? Please pick a preset:
Default ([Vue 2] babel, eslint)
❯ Default (Vue 3 Preview) ([Vue 3] babel, eslint)
Manually select features
(If you don’t see the Vue 3 option, make sure you have the latest Vue CLI installed.)
Finally, cd into the project and start the development server:
cd swr-app
npm run serve
For this Vue app, we’re going to fetch the data from an API server, so let’s create such a server now.
JSON Server
To quickly create a JSON server, we can make use the json-server
NPM package.
We have to install it globally because it’s a CLI tool:
npm install -g json-server
(You might have to run this command with sudo
if you run into some permission error.)
Next, prepare a db.json file, with some sample data.
{
"articles": [
{
"title": "GraphQL Client with Vue.js",
"author": "Andy",
"link": "https://www.vuemastery.com/blog/part-3-client-side-graphql-with-vuejs",
"id": 1
},
{
"title": "TypeScript with Vue.js",
"author": "Andy",
"link": "https://www.vuemastery.com/blog/getting-started-with-typescript-and-vuejs",
"id": 2
}
]
}
This will be used as our “database”. json-server
will use this json file to create a mock API server on the fly.
Now let’s start the server in the console.
json-server --watch db.json
(The --watch
flag tells json-server
to always serve the most up-to-date data from db.json.)
It should be up and running at port localhost:3000.
To test this new API server, we can open a browser console and just send the following request to get all the articles
data.
fetch('[http://localhost:3000/articles](http://localhost:3000/articles)')
.then(r => r.json())
.then(data => console.log(data))
(json-server
enables CORS by default, so you don’t actually have to make the request from the same port on the same domain.)
And we can make a POST request to add data to the server
fetch('[http://localhost:3000/articles](http://localhost:3000/articles)', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"title": "VS Code for Vue Developers",
"author": "Andy",
"link": "https://www.vuemastery.com/blog/vs-code-for-vuejs-developers"
})
})
If we open the db.json file we created earlier, we’ll see this new entry.
{
"articles": [
{
"title": "GraphQL Client with Vue.js",
"author": "Andy",
"link": "https://www.vuemastery.com/blog/part-3-client-side-graphql-with-vuejs",
"id": 1
},
{
"title": "TypeScript with Vue.js",
"author": "Andy",
"link": "https://www.vuemastery.com/blog/getting-started-with-typescript-and-vuejs",
"id": 2
},
{
"title": "VS Code for Vue Developers",
"author": "Andy",
"link": "https://www.vuemastery.com/blog/vs-code-for-vuejs-developers",
"id": 3
}
]
}
(json-server
automatically assigns the id
field for us, we didn’t have to send that over from the client-side.)
This means our API server is working!
If we want our Vue app’s development server interacting with this API server, we have to configure the development server as a proxy server. Proxy just means that it’s able to accept requests that it can’t process itself and pass these requests to another server. In our case, we want the Vue development server to pass all the API requests to the API server.
To set up the proxy, we just need to create a vue.config.js file in the project directory and put this inside:
📃 /vue.config.js
module.exports = {
devServer: {
proxy: 'http://localhost:3000'
}
}
(localhost:3000 is where the API server is running.)
Finally, all the API server setup is done!
Fetching with SWRV
Now that we have a functional API server, let’s try using it in our Vue app with SWRV.
First, we have to install the SWRV library
npm install swrv
Inside the App.vue file, we’ll import the useSWRV
function. This is the main function of the library.
📃 /src/App.vue
import useSWRV from 'swrv'
export default {
name: 'App',
...
To demonstrate the core fetching and caching feature, let’s fetch some data from our JSON Server in the setup
function.
📃 /src/App.vue
...
setup() {
useSWRV('/articles', fetcher)
},
SWRV doesn’t actually dictate how you’re getting the data, it just requires a function that returns a promise object. That’s the fetcher
function as the second argument. So let’s create this function next.
📃 /src/App.vue
// ADD
const fetcher = function(url){
return fetch(url).then(r => r.json())
}
export default {
name: 'App',
...
It’s really just a normal function that takes a url
as the parameter, fetches the data from the server with the url
, extracts the json
from the server response, and finally, returns the json
data as a promise object.
Since we provided the url
as the first argument, SWRV will feed that url to our fetcher
as a parameter. So what’s the point of giving the url
to useSWRV
just for it to get passed to the fetcher
? Why not just decide what the url
is inside the fetcher
?
There are two reasons for this arrangement:
- First, this
fetcher
is meant to be an abstraction over your fetch library of choice. You can use the genericfetch
function, or a library like Axios. Then the fetcher function can be used for fetching from different URLs. - The second and more important reason is that the
url
string will be used as the cache key. SWRV will rely on this key to retrieve the corresponding data from the cache if it’s been cached previously.
Next, the useSWRV
function will return a few items. For now, let’s just focus on the data
and error
.
📃 /src/App.vue
...
setup() {
const { data, error } = useSWRV('/articles', fetcher)
},
Depending on the current status of the request, data
can either contain the actually requested data or just undefined
, while error
can either contain an error message
property or just undefined
. Both of these variables are ref
objects, so the data has to be accessed through data.value
.
error
is useful for displaying a “Sorry, there’s been an error” type of message conditionally.
We can rename them using object destructuring assignment, you don’t have to use these generic names if you don’t like.
📃 /src/App.vue
...
setup() {
const { data: articles, error: articlesError } = useSWRV('/articles', fetcher)
},
And finally, return the two variables so we can use them in the template:
📃 /src/App.vue
...
setup() {
const { data: articles, error: articlesError } = useSWRV('/articles', fetcher)
// ADD
return {
articles,
articlesError,
}
},
Now, we can just display the content in the template (optionally, with a conditional error message):
📃 /src/App.vue
<template>
<div>
<p v-if="articlesError">Sorry, there's been an error</p>
<ul>
<li v-for="item in articles" v-bind:key="item.id">
<a v-bind:href="item.link" target="blank">{{ item.title }}</a>
</li>
</ul>
</div>
</template>
If you run the app in the browser, you should be seeing a bunch of article items:
To recap, here’s our code so far:
📃 /src/App.vue
<template>
<div>
<p v-if="articlesError">Sorry, there's been an error</p>
<ul>
<li v-for="item in articles" v-bind:key="item.id">
<a v-bind:href="item.link" target="blank">{{ item.title }}</a>
</li>
</ul>
</div>
</template>
<script>
import useSWRV from 'swrv'
const fetcher = function(url){
return fetch(url).then(r => r.json())
}
export default {
name: 'App',
setup() {
const { data: articles, error: articlesError } = useSWRV('/articles', fetcher)
return {
articles,
articlesError,
}
},
}
</script>
Caching
Next, let’s demonstrate how data are fetched from the local cache instead of the actual server. We will make two different components fetch from the same URL. The first one will hit the actual server, the second one will hit the cache instead.
To demonstrate the stale-while-revalidate concept, let’s create a second component, and put it inside the components folder.
📃 /src/components/Count.vue
<template>
<div>
<span>Article Count: {{ articles === undefined ? 0 : articles.length }}</span>
</div>
</template>
<script>
import useSWRV from 'swrv'
const fetcher = function(url){
return fetch(url).then(r => r.json())
}
export default {
name: 'Count',
setup() {
const { data: articles } = useSWRV('/articles', fetcher)
return {
articles
}
},
}
</script>
This code is similar to App.vue in that they’re both fetching from the same URL /articles
. But different from the first component since it’s displaying the number of articles instead of the articles themselves. I also left out the error
for simplicity.
Since the fetcher is the same function for both components, we should extract it to its own file.
📃 /src/fetcher.js
const fetcher = function(url){
return fetch(url).then(r => r.json())
}
export fetcher
In Count.vue, we can just import the fetcher:
📃 /src/components/Count.vue
import useSWRV from 'swrv'
import fetcher from '../fetcher.js' // ADD
export default {
name: 'Count',
Back in App.vue, let’s import and use the new component and fetcher file:
📃 /src/App.vue
import useSWRV from 'swrv'
import Count from './components/Count.vue' // ADD
import fetcher from './fetcher.js'
// const fetcher = function(url){
// return fetch(url).then(r => r.json())
// }
export default {
name: 'App',
components: { Count }, // ADD
setup() {
const { data: articles, error: articlesError } = useSWRV('/articles', fetcher)
return {
articles,
articlesError,
}
},
}
Render the Count
element in the template
:
📃 /src/App.vue
...
</li>
</ul>
<Count></Count>
</div>
</template>
If the cache works as expected, the fetcher
function will run only once, then the second time that same API request is made inside the second component, the first request’s cached data will be served instead. So the second time, the fetcher function wouldn’t even get called.
We can confirm this by adding a console.log
inside the fetcher
function:
📃 /src/fetcher.js
const fetcher = function(url) {
console.log('fetching ...'); // ADD
return fetch(url).then(r => r.json());
}
Run the app and check the browser console, you should be seeing just one log message from the fetcher.
The SWRV library is using the URL as the cache key to figure out whether or not you’re asking for the same data. Since both useSWRV
calls are using the same URL as the cache key, they are recognized as the same thing by SWRV, so it just returned the cached data.
Revalidation
Revalidation means refreshing the cache so all elements on the page are showing the latest updated data. The default behavior of SWR is that every subsequent request with the same cache key as a previous request will reserve the cached stale version of the data first, then go on to do the fetch, then replace the cached stale version with the freshly updated version.
But as we demonstrated earlier, the second request only got served from the cache, it didn’t hit the server. That’s because the fetcher
is asynchronous, so when the second fetch was initiated in the Count
component, the first fetch hadn’t yet been resolved. Since it started before the first fetch was resolved, the second request isn’t considered “subsequent”; hence, it shared the same fetch response without doing another fetching over the network.
To get two separate fetches, we can set up a timer to render the Count
component a few seconds later just to make sure it’s only rendered when the first fetch request has been resolved.
First we’ll set up a boolean ref
, initialize it to false
, and then set it to true
in three seconds:
📃 /src/App.vue
import { ref } from 'vue' // ADD THIS
import useSWRV from 'swrv'
import Count from './components/Count.vue'
import fetcher from './fetcher'
export default {
name: 'App',
components: { Count },
setup() {
const { data: articles, error: articlesError } = useSWRV('/articles', fetcher)
// ADD THIS
const showCount = ref(false)
setTimeout(() => showCount.value = true, 3000)
return {
articles,
articlesError,
showCount, // ADD THIS
}
},
}
(Since showCount
is technically a ref
object, we have to change it by changing its value
property.)
And then use the showCount
ref to conditionally render the Count
element:
📃 /src/App.vue
...
</ul>
<Count v-if="showCount"></Count>
</div>
</template>
Refresh the app in the browser, and pop open the browser console, you should see two console log messages from the fetcher
function.
This proves that subsequent fetches will still hit the server even when a cached version of the needed data is available.
Another interesting thing is: when you switch to another browser tab and switch back, you’ll see that the fetcher gets run again (check the console log messages). This is another default behavior of SWRV that all items in the cache get revalidated (refreshed) on focus. “Focus” means switching back to the page from another tab or window.
To recap, here’s our code in App.vue:
<template>
<div>
<p v-if="articlesError">sorry, there's been an error</p>
<ul>
<li v-for="item in articles" v-bind:key="item.id">
<a v-bind:href="item.link" target="blank">{{ item.title }}</a>
</li>
</ul>
<Count v-if="showCount"></Count>
</div>
</template>
<script>
import { ref } from 'vue'
import useSWRV from 'swrv'
import Count from './components/Count.vue'
import fetcher from './fetcher'
export default {
name: 'App',
components: { Count },
setup() {
const { data: articles, error: articlesError } = useSWRV('/articles', fetcher)
const showCount = ref(false)
setTimeout(() => showCount.value = true, 3000)
return {
articles,
articlesError,
showCount,
}
},
}
</script>
Mutation
So we now know the cache gets refreshed from time to time when the window or browser tab is focused. We also have the choice of actively refreshing the cache. This is called a mutation.
Aside from data and error, useSWRV
also returns a mutate
function. We can use this function to trigger a revalidation for the associated cached data (articles
in this case).
📃 /src/App.vue
setup() {
const { data: articles, error: articlesError, mutate: mutateArticles } = useSWRV('/articles', fetcher)
...
(For consistency, we renamed this mutate
function to mutateArticles
.)
Then, we can set up an event handler that calls the mutateArticle
function:
📃 /src/App.vue
setup() {
const { data: articles, error: articlesError, mutate: mutateArticles } = useSWRV('/articles', fetcher)
// NEW
function updateArticles(){
mutateArticles()
}
...
return {
articles,
articlesError,
showCount,
updateArticles, // NEW
}
},
And when the button
is clicked, it will trigger the mutation:
📃 /src/App.vue
...
<button @click="updateArticles">Update Articles</button>
</div>
</template>
When you click on this button in the browser, the fetcher will get called and update the cache with new data. And if the cache is updated, all the UI elements relying on the cache will get updated, too.
Since our server data doesn’t change by itself, you won’t see any difference when you click the button. But, if you still have that console.log
inside the fetcher
, you should see a log message ("fetching …") in the browser console every time you click on the button.
To clear up some confusions, the meaning of the term mutation here is different from what you might be used to. SWRV*'s* mutation isn’t about making a POST request to change some data on a remote server. Although, in practice, these two operations are usually paired together.
To recap, here’s our App.vue:
<template>
<div>
<p v-if="articlesError">sorry, there's been an error</p>
<ul>
<li v-for="item in articles" v-bind:key="item.id">
<a v-bind:href="item.link" target="blank">{{ item.title }}</a>
</li>
</ul>
<Count v-if="showCount"></Count>
<button @click="updateArticles">Update Articles</button>
</div>
</template>
<script>
import { ref } from 'vue'
import useSWRV from 'swrv'
import Count from './components/Count.vue'
import fetcher from './fetcher'
export default {
name: 'App',
components: { Count },
setup() {
const { data: articles, error: articlesError, mutate: mutateArticles } = useSWRV('/articles', fetcher)
function updateArticles(){
mutateArticles()
}
const showCount = ref(false)
setTimeout(() => showCount.value = true, 3000)
return {
articles,
articlesError,
showCount,
updateArticles,
}
},
}
</script>
POST Request
Instead of having a button to update the cache, let’s make the button send a POST request to add new data on the server.
First make the updateArticles
function async
and use await
to make a POST request with fetch
:
📃 /src/App.vue
async function updateArticles(){
await fetch('http://localhost:3000/articles', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"title": 'Untitled - ' + new Date().toString(),
"author": "Andy",
"link": "https://vuemastery.com/blog",
})
})
mutateArticles()
}
(We’re using the Date
object to generate some unique article titles so you can click on the button many times to add new data items with unique titles.)
The reason we await
the fetch is to make sure that mutateArticles
runs after everything is updated on the server.
Try clicking on the button in the browser, if you see the article list gets appended with a new item, this has been a successful experiment.
Centralized Store
At this point, you might be wondering… How does this SWR thing fit in with centralized store modules such as Vuex, Mobx, or Redux?
As you can see with our code so far, SWRV isn’t really about fetching (the fetching is still done through fetch
or any fetching library you choose). Instead, it’s about caching and revalidating that cached data.
A centralized store on the client-side is technically just a cache. So tools like SWRV are meant to replace tools like Vuex. On top of serving as a centralized store, SWRV makes it easier to work with a remote API.
But what I’ve been referring to so far is about application states that are based on an API server. What about application state that isn’t based on a remote API? Local data that never needs to bother the server? In that case, we can still use Vuex alongside SWRV. But … SWRV actually allows you to work with local data, too. It doesn’t always have to be about data over HTTP.
The fetcher function can also just return a non-promise value, like string, number, or array.
📃 /src/App.vue
export default {
name: 'App',
components: { Count },
setup() {
const { data: articles, error: articlesError, mutate: mutateArticles } = useSWRV('/articles', fetcher)
// NEW
const { data: hideList, mutate: mutateHideList } = useSWRV('hideList', () => [])
Here we created a local state hideList
and initialized it with an empty array. Let’s allow the user to click on a Hide button next to each article, and then add that article’s id
into this hideList
array (technically a ref
object that contains an array).
Create a computed property with computed
:
📃 /src/App.vue
import { ref, computed } from 'vue' // CHANGE THIS
import useSWRV from 'swrv'
import Count from './components/Count.vue'
...
export default {
name: 'App',
components: { Count },
setup() {
const { data: articles, error: articlesError, mutate: mutateArticles } = useSWRV('/articles', fetcher)
const { data: hideList, mutate: mutateHideList } = useSWRV('hideList', () => [])
// ADD THIS
const visibleArticles = computed(function() {
if(articles.value === undefined) {
return articles.value
}
else {
return articles.value.filter(a => hideList.value.indexOf(a.id) === -1)
}
})
...
(The conditional is for making sure that the data in article
is resolved before doing the filtering.)
visibleArticles
will contain the articles that are not in the hideList
, so by default, visibleArticles
should have the same items as articles
.
Next, create a Hide button next to each article title in the template
:
📃 /src/App.vue
...
<li v-for="item in visibleArticles" v-bind:key="item.id">
<a v-bind:href="item.link" target="blank">{{ item.title }}</a>
<button @click="hideArticle(item.id)">Hide</button>
</li>
...
When the Hide button is clicked, it will trigger the hideArticle
function, passing in the article’s id.
Let’s create this function inside setup()
:
📃 /src/App.vue
setup() {
...
const visibleArticles = computed(function() {
if(articles.value === undefined) {
return articles.value
}
else {
return articles.value.filter(a => hideList.value.indexOf(a.id) === -1)
}
})
// ADD THIS
function hideArticle(id){
mutateHideList(() => hideList.value.concat([id]))
}
Finally, return visibleArticles
(instead of articles
) and hideArticle
so they can be used in the template:
📃 /src/App.vue
setup() {
...
return {
visibleArticles, // CHANGE
articlesError,
showCount,
updateArticles,
hideArticle, // NEW
}
},
Now, the article-hiding feature is done, and it’s entirely local without hitting the server.
Although SWRV isn’t meant to be used for offline local data, it can be used this way if you really need it.
Here’s our final code:
📃 /src/App.vue
<template>
<div>
<p v-if="articlesError">sorry, there's been an error</p>
<ul>
<li v-for="item in visibleArticles" v-bind:key="item.id">
<a v-bind:href="item.link" target="blank">{{ item.title }}</a>
<button @click="hideArticle(item.id)">Hide</button>
</li>
</ul>
<Count v-if="showCount"></Count>
<button @click="updateArticles">Update Articles</button>
</div>
</template>
<script>
import { ref, computed } from 'vue'
import useSWRV from 'swrv'
import Count from './components/Count.vue'
import fetcher from './fetcher'
export default {
name: 'App',
components: { Count },
setup() {
const { data: articles, error: articlesError, mutate: mutateArticles } = useSWRV('/articles', fetcher)
const { data: hideList, mutate: mutateHideList } = useSWRV('hideList', () => [])
const visibleArticles = computed(function() {
if(articles.value === undefined) {
return articles.value
}
else {
return articles.value.filter(a => hideList.value.indexOf(a.id) === -1)
}
})
function hideArticle(id){
mutateHideList(() => hideList.value.concat([id]))
}
async function updateArticles(){
await fetch('/articles', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"title": 'Untitled - ' + new Date().toString(),
"author": "Andy",
"link": "https://vuemastery.com/blog",
})
})
mutateArticles()
}
const showCount = ref(false)
setTimeout(() => showCount.value = true, 1)
return {
visibleArticles,
articlesError,
showCount,
updateArticles,
hideArticle,
}
},
}
</script>
📃 /src/components/Count.vue
<template>
<div>
<span>Article Count: {{ articles === undefined ? 0 : articles.length }}</span>
</div>
</template>
<script>
import useSWRV from 'swrv'
import fetcher from './fetcher'
export default {
name: 'Count',
setup() {
const { data: articles } = useSWRV('/articles', fetcher)
return {
articles
}
},
}
</script>
📃 /src/fetcher.js
const fetcher = function(url){
return fetch(url).then(r => r.json())
}
export fetcher
Configurations
If you click the hide buttons one after another fast, you’ll notice that the click is sometimes ignored. That’s because the mutation is deduped with two seconds by default. Deduping means no duplicated requests can be run within a certain interval of time, and in this case, it’s two seconds. This can save server workload for fetching data from a remote server; it prevents unnecessary requests when the user is impatient while clicking on buttons repeatedly.
Now since the hideList
state is local data, we don’t need any network action, hence we don’t need deduping. To make the Hide buttons work properly even when they’re clicked one after another in a split of a second, we can turn off the default two-second deduping by setting it to 0 in the third argument for useSWRV
.
const { data: hideList, mutate: mutateHideList } = useSWRV('hideList', () => [], {
dedupingInterval: 0
});
You can configure the behavior of SWRV using other options, too, such as revalidateOnFocus
and refreshInterval
.
You can set revalidateOnFocus
to false
if you don’t want the cache to be updated every time you jump to the browser window/tab from your other windows/tabs.
You can set refreshInterval
to the number of milliseconds you want the data to get refreshed periodically. If you set it to 2000, the cache will get updated every two seconds. By default, the refreshInterval
is 0, which means it’s off.
You can check out the full list of configuration options on the library’s GitHub page.
What’s next
Now that you’ve gotten the fundamentals of SWRV, it will be easy for you to pick up a similar tool in the future. SWRV is just one of many libraries that employ the stale-while-revalidate scheme. For instance, Apollo GraphQL Client can be configured to use a cache-and-network fetch policy, which is basically stale-while-revalidate under a different name. Also as a future reference, Nuxt.js’s version 3 will feature a data fetching function similar to useSWRV
. So stay tuned.