Version 3 is just around the corner and the composition API is bringing some new exciting possibilities, like building React Hook-like functions to help manage and share common logic and functionality.
One of the cases where we might take advantage of hooks — and that seems to be in the spotlights theses days — is the use of SWR, Stale While Revalidate. It’s a strategy that to keep the balance between immediacy — loading cached content right away — and freshness — ensuring updates to the cached content.
Modern browsers these days already have support to use this feature on the fly, but for this, the API must send specific headers in the response. You can learn more about this approach in this article.
The problem with this approach is that sometimes you use someone else’s APIs, and changing response headers is not a viable option. To solve this, we will build our own custom hook that can be re-used across all your components.
So let’s get our hands dirty and build a simple solution to this.
To start let’s define what we’ll be doing. I’ve made a simple flowchart to explain how this hook will work:
We’ll receive a key to identify the request and the promise to be resolved. Then we check if the key already exists in the cache. If so, we inform the caller of the cached value. Then we resolve the promise (if we have the cached result or not) and inform the caller the result: If it’s a success we update the cache and inform the caller of the updated value, otherwise, we inform the caller we had an error resolving the promise.
You might ask why it’s a promise and not the URL of the API that we’re calling. By using a promise as input and not the URL we’re giving the option for this hook to be used in any case the result depends on a promise, whether it’s an API call or not. And even if it will be used only for API calls, we’ll keep the caller’s right to choose which approach will be used: The native fetch API, Axios, jquery’s AJAX , Angular’s $http or any other amongst the many solutions available on the internet.
To make our little project we’ll use Vite. It’s a development server and production bundler started by Evan You (vue’s creator) that serves the code using ES modules import and bundles the code using Rollup (a bundler created by Rich Harris, creator of Svelte) for production. It’s much faster than using the traditional vue-cli’s webpack based approach, especially in development mode. Since there’s no bundling involved, the server start and browser refresh is almost immediate.
To start our project we need to have node installed (if you don’t, click here to download and install the LTS version) and I would recommend installing yarn (learn how to that it here), a package manager that replaces npm (node’s native package manager), since yarn is faster than npm in most occasions.
With node and yarn installed, go to your terminal in the root folder where you want to create your project and use this command:
yarn create vite-app my-swr-hook
After a few seconds, the process is done, and we can install all the dependencies and run the project using the commands bellow:
cd my-swr-hook yarn yarn dev
Now just open your browser and navigate to http://localhost:3000 to check the default application running.
Now it’s time to build our custom hook. We create a hooks folder inside src and then create a swr.js file.
We’ll start by creating a global cache and the function that will be exported and make all the work we need. By putting the cache outside of the returned function we ensure that it’s unique and accessible to all callers. The function will receive a key, and a promise, and will return the cached value if it exists. After that, we’ll resolve the promise and update the cache and/or return the corresponding response. Well use named export for the function (just a personal preference):
We get a big problem with this code because whether we have or don’t have the cached value, we’ll resolve the promise and return the updated value (or error). But in our piece of code, if we get the cached value it’s returned and that’s it. With this approach, we can’t move on and resolve our promise to revalidate the cache. Another problem is that we’re returning two kinds of response, one is pure data (from the cache) and the other one is a promise. And the error treatment is a little bit rough.
To make this work we’ll use Vue’s composition API ref. This utility creates a reactive and mutable object. By using this all we have to do is return the reactive constant and the callers will be notified of changes. We’ll start this constant with the cache’s key-value or null (in case the key doesn’t exist). To avoid the possibility of the caller changing our state, we’ll use another composition API functionality, readonly. The second version of our hook code now looks like this:
It’s a lot better, but there’s still room for improvement. I think we can add and optional parameter to load the initial state (in case is not already in the cache) and return other parameters so that the caller knows if we are revalidating, if an error has occurred (and which error was that). Since now we are returning multiple values, it’s a better idea to create a state object with all the keys inside and update them accordingly. In this case, reactive is more suitable then ref. Another change we’ll have to do to make it possible for the caller to use destructuring and get individual reactive values is to make use of composition API utility toRefs.
Another feature I think would be cool is to add localStorage. With this addition, if the key has already been called anytime in the past, the user will be instantly provided with the data. To make the state saving automatic whenever the data change we can use watchEffect. We’ll wrap the localStorage’s setItem method in a try-catch to avoid problems when the fetched data exceeds the quota, which will make our application stop working.
With these final changes, our custom hook is ready to be used.
Let’s create a new file in the components folder, Libraries.vue. This component will be responsible to fetch and render the libraries list. We’ll use the composition API and dispatch an event when the user clicks on any item, so the App component may know what library is selected and therefore trigger the library detail fetch and render.
Now let’s change our App.vue file to render the list. We’ll add a selected ref too to receive the event dispatched from the Libraries component.
You’ll notice that the first time you load the app, the Library component will show Loading, and some seconds later the list will be rendered. Since we’ve stored the data in browser’s localStorage, from the second time on the list will be rendered immediately. But if you open network tab in the browser’s developer tools, you’ll notice that each time you refresh the page the request will still be made in the background. If the returned data is different from the stored one, the list and localStorage value would be updated by our swr hook.
So now let’s build our Library component, which will be responsible to fetch and render de information about the selected library. This information will be received by props passed from the App component. We’ll render just some of the info provided by cdnjs. If you want to inspect the returned data format you can check the vue link here.
With the Library component ready, it’s time to change our App component so that if a library is selected the Library component is rendered. One special point of attention here is that if we use the Library component in the template, it will be rendered just once and only fetch the information about the first selected library.
There are many ways to solve this, like adding a watch to the name prop in the Library component, but there’s an easier way: use the key prop. If we add a key prop tied to the selected library name every time we select a new library the key is updated and the Library component is re-rendered, solving our issue.
So our App component will look like this:
Like on the Library component, you’ll notice that the first time you click on a library, the loading message is displayed, and shortly after the library info is rendered. If you click on another one and then click back on one you’ve already clicked, the info will be rendered immediately and the fetch request will be made in the background to check if the response is still the same.
With this we’ll have achieved our objective — to present the data as soon as we can to our client, re-validate it in the background and update it. You may do some improvements, like adding a time-to-live parameter so that the re-fetching will be done just after it, or add some extra error checking. I’ll leave this as homework: Make new implementations to make this code suitable for your needs.
The source code of the working application is available in my github.
Any suggestions or observations are welcome as always.
Hope you liked the article and learned something new.
See you next article.