Local State and Server Cache: Finding a Balance


How many times did you implement the same flow in your application: check, if data is already fetched from the server, if yes - render the data, if not - fetch this data and then render it? I think I've done it more than ten times myself and I've seen the question about this flow more than fifty times. Unfortunately, our go-to state management library, Vuex, doesn't provide any solution for this.

For GraphQL-based application, there was an alternative to use Apollo client that provided tools for working with the cache. But what if you use REST? Luckily, now we have a Vue alternative to a react-query library that provides a nice solution for working with server cache. In this talk, I will explain the distinction between local application state and local server cache and do some live coding to show how to work with the latter.


Hello everyone! Happy to see you all today at this event. And while I really hope that this conference is going to be offline this year, unfortunately for us all, I'm speaking with you remotely again from Amsterdam. Maybe next year? Let's hope for it. And today we're going to talk about a topic that is an endless source of frustration for all the developers, which is handling a local state of our application in the cases when it needs to deal with asynchronous behavior, api requests, and all like this. Let me quickly reintroduce myself. My name is Natalia Dipluhina, I'm vue.js core team member. I work mostly on vue documentation, so if you were reading vue 3 docs, there are high chances that you were reading something that I authored. I work as a Stack frontend Engineer at GitLab, and there at GitLab, of course, we have vue in our frontend stack. But for state managers, we deal both with vuex, that will be briefly touched in this talk, and apollo Client, that will be briefly touched in this talk as well. So I know the pains of each solution, and I have some idea about what I'm talking in this particular talk. And I'm also a Google Dev expert in web technologies. So let's start diving into our global state. So what is a global state in our application? This is some data that we want to share across different components, different views, different routes, and for some reason, we need it available everywhere in our application. Let's look at a very short, brief, quick example for those who never worked with vuex or forgot how it looks like, and this will be vuex 4. So if syntax is a bit unfamiliar to you, don't be afraid. It's very similar to vuex 3 that we use with vue 2. So imagine that we have a local state that returns one single property called count, and this is zero. And we need to change this property somehow, so we add a mutation, because all the state properties should be only changed with mutations. So we have a mutation for incrementing the count that just increments the count, suggesting by name. And this is already enough to use this in our component, but some companies, like my own one, has a convention that says that we cannot commit mutations from components. If you ever heard this is an anti-pattern, this is incorrect. You can call mutations, commit mutations from components, but it may make your factoring harder when you need asynchronous behavior. That's why sometimes we wrap them in actions. So this is sufficient boilerplate for incrementing count. What we are dealing with here in this particular example. First of all, we deal with asynchronous changes in the state. That's why I said that probably we won't even need an action. We don't have any asynchronous behavior here. It's non-persistent, so whenever I update the page, count will be zero again. We don't need to store this anywhere else outside of the vuex state. That's one source of changes, so this is like our magical single source of truth. That only is responsible for changes of count in our application. Count can be changed anywhere else. It's stored only in vuex and changed only in vuex. And yes, it does not create a lot of boilerplate. This point can be a bit debatable, because three different properties for changing one count, still, it's a reasonable amount of boilerplate for this change, considering the fact that count will be shared all across the application. But all of this is not as nice when we start dealing with server data. So let's add some server data to the equation in our vuex state. Here at the first look, it looks like the same count. We have property characters, and we have a mutation that changes characters, and we have some asynchronous action that fetches data from our characters endpoint. And after this, we commit the mutation, so everything is fine. But with this, your application is not sufficient, because fetching characters is an asynchronous operation, which means that there will be a moment in your app when there are no characters and you're idle, you're waiting for your api to return your data. And of course, you add loading state, because you need to show this nice loading spinner, a skeleton, or whatever you're showing in your application. And also, we are fetching data from the api. And what if response is not there? What if your request wasn't successful? We need to handle errors, which is a super good practice for any asynchronous call. And that's why we need to add one more flag, error, to our application. And with this, our simple structure of one action, one mutation becomes not as simple, because you need to update loading, you need to update error. And here is quite a common convention, GitHub uses it as well, to handle asynchronous behavior with requests. So here we have three finite states, a loading state, a success state, and an error state. And here we are setting loading to true, error to false, so we are in a loading state now. Then we receive our characters, everything is fine, loading is false, error is null, characters are here, and we render the array of characters. And of course, there can be some error state. So in this particular case, we set loading to false, we say that error is something that server returns to us, characters select, three mutations. Yes, you can simplify it if you have the only one, but if you want some kind of standard, usually companies come with something like this. And now our action is not as simple as well, because when we start an action, we go into this loading limbo state, requesting characters, and then we will have characters, we commit the success mutation, storing the characters to the state, and if there is an error, we also commit an error mutation. And if you imagine all of this boilerplate, it's quite big already. So it's huge for one single request. But if we have multiple requests, probably we have different loading states, or should it be the same loading state? Every single time we do this, you need to rethink it, and to understand if we want to share loading states between different requests in our application, and this still doesn't answer a bunch of questions. How to fetch only when data is not in vuex? I heard this request like 50 times probably, how to write a getter that will return me my data if data is in vuex, and will dispatch an action if data is not in vuex. There is no such getter. Getter is only getter, we don't want to commit a side effect. That's why every single time we need to implement this behavior in our application, we come with the same set of wheels. We do this check in the component, if this is in this state, I'm not going to dispatch an action. Or we dispatch an action, then action checks the state, like, okay, we are calling Axios, or we just return, because we already have data in our component. This is a bit annoying, because you need to re-implement this for such a common case in your app. The second one is how to keep my data up to date. In this particular case, we're still thinking about vuex as a single source of truth, but it's not, because your source of truth is on the api. And your vuex right now is only a reflection of the server. I call it a server cache, server data, server state. This state belongs to the server, and we try just to have this reflection on our frontend. And we need to keep this reflection up to date. If you're lucky, you have WebSockets. So on WebSockets event, you update your vuex state. For those of us who cannot allow themselves WebSockets, we do polling. Every 30 seconds, we send this request to the server, triggering all this change of actions, mutations, loading state, handling errors, and then we update something in vuex. Well, that's sometimes challenging. And then, what if two components start fetching the same query simultaneously? Nothing will explode, honestly, but you will have two requests, because vuex has no built-in behavior for deduplicating requests. They will be both resolved. Action will be triggered twice. Why is it all happening? Why do we have so many problems with asynchronous behavior in vuex? Because we are dealing not with the local state anymore. We are dealing with something different. Something that, first of all, is asynchronous, and we need to deal with all the bells and whistles of asynchronous behavior. So loading state, error states, and all these different finite states in our application, depending on this behavior. Something that is persistent remotely. Something that has a source of truth outside of our control as a front-end developer. And that's why it has a shared ownership. Our database on the server can be updated from somewhere else. It can be updated by a back-end developer, but it can be also updated from the other instance of our front-end client. And we need to deal with this behavior. We need to reflect changes to server to changes to client. And unfortunately for us, vuex is not designed with this in mind. Like Redux wasn't designed with this in mind for react. This is not specifically a vuex issue. This is an issue for most of the state managers. Not all states are born equal. It's good to think about what we consider a global state. Something that is separated to our local state. So the synchronous data that are persisting only in applications, something like form data when you're filling the form. And server cache. Something that is present on the remote api. And you can say, what are alternatives to this? So let's first consider this quick example of vuex in the application that I built. So let's do this. So I have an application running. Let me quickly check on what particular address we are going. So starting the task. Yep, 3001. Here we go. And I like Dune. I like the recent movies, so I built a fake api just for a few of Dune characters. And what we have here, we are fetching the array of characters on our main page. Here it goes. And when I go to some of the routes, I fetch one more character. What is happening in the application in this particular moment? First of all, all the characters are stored in vuex. So here goes my app. And let's check our store. You can see I have characters, loading, and error flags. We are all set. And we have this set of mutations. I was trying to abstract this as much as possible. I have the same loading state for character and characters and error state as well. That's why I have this set loading and set error state, set characters, and set character, which is my favorite part. Because here we are selecting a character from the characters array by index. And then we enrich the data. Because what happens here? Let's go to our characters page and see. So our characters here is an array of things that only contains normally an ID, a name for every single character. And when I go to this route, I have a bunch of data, like description, section, when a character was born. And when I go back, I have like Paul and Channy. We checked them both in the application. They are enriched by the character call. And this is super fragile. Because what I'm doing in my character view is I check... This is super funny, by the way. I need to check if character already has a description. Because if it does, I don't want to call an action for fetching character, exactly what I was explaining. So I'm checking if description is there. And if it's not, I dispatch fetch character and show the character in my api. But I'm relying on the fact that characters array is already fetched. So I believe that this request is resolved faster than my character. And so far, it works just fine in my application. So if I go to Paul, and you can see that characters is resolved faster than the single character. But I can imagine the case when characters fetches thousands of records. And what if it's resolved after? I cannot rely on this. This is asynchronous behavior. I built something super, super fragile on my frontend part. And also, look at this, when I'm fetching Paul, I always fetch this character list first. I don't even need it. I need only Paul to trade this. Why am I fetching all that? Because my tool is not perfect. What are alternatives to this? Well, if you work with graphql, you probably know apollo Client. And I already mentioned it during this talk. apollo Client is amazing if you work with graphql. And if you use it with vue 3, the syntax will look something like this. You have the use query from your apollo composable. And whenever you call the api, because this will trigger an api call, our graphql api, you have result loading an error and many more properties. But so far, we need only this. And I can display them in my component. The best part, and this query stores the response to cache. So whenever I call it again from this component or any other component, by default, it will first hit the cache. And then if cache is empty or something, it will call an api. This behavior can be changed in apollo Client settings. But by default, everything is in cache. It's cached first. If you wonder what is characters query GQL, if you've never worked with graphql, this is just a graphql query. And apollo Client is technically capable of working with REST api. You can create a resolver. So whenever you call a characters query, instead, you're just doing the call for your REST api with Axios and return something that is shaped properly for our query, which is still valid, but it's a bit of boilerplate. And then you can also have your local state in apollo Client. So imagine if you want to add character, you would need to do all of this. You will read the query from the cache, creating your character, creating your data object and you because apollo Cache is immutable. And then you write query down back to the cache. It's fine if your application doesn't have a rich local state. If it's something super server cache based and you need only to fetch data from the server and update them with mutations. With a local state, it's questionable. The boilerplate here is probably bigger than in vuex and your developers are most likely to complain about vuex was better. The problem here is the one size doesn't fit all. We have vuex, which is super nice and handy in local state and it's kind of problematic with dealing with server, as you can see in this talk. But we also have apollo Client, which is great when you work with server, mostly with graphql api. And it's a bit more questionable when you need to deal with a local state. So maybe our desire to have a single source of truth is not as great. Maybe we handle two different entities and it would be nice to have two different tools. So if we leave vuex for the local state, there is a great tool for react that exists for a longer time. It's called react Query by Tanner Linsley. But for vue, it was created recently because Tanner exposed the core of react Query and there was a vue query wrapper created by Damian Osipiuk. And I want to have a little demo of this tool right now in my talk. So I will switch, remove this, and switch to the initial branch. And let's use vue query here. So in our home vue, we will import the method called useQuery from our vue query library. And we also will need our getCharacters method. So I will import getCharacters from api.js. And this is just an access call to my local host. So here I will start using my useQuery. First, I want to return some data from useQuery. And here, first argument is a key for the query, characters. And second is my fetcher function, getCharacters. Let's return data from the setup and see how it looks like in our application. Here are my dev tools. I go to the characters. And data is an object that contains a data array of 10 characters. So with this, I can use the magic of the computed and say that const characters, oh, it's already here. Const characters is a computed property based on data, the value, because this is a ref. Please always remember to use value with refs, data. And here goes my list of characters. I can remove returning data here now. And also, I can expose isLoading and isError here in my app. Instead of creating these two refs. And right now, there is a super short moment when you can see loading here blinking. And if I make a mistake in my api, interesting moment, it will be a few retries that useQuery does by default. And after this, it will say, OK, there is an error right here on the page. I really like the idea about retries, because mostly when there are issues with connectivity, we want some kind of retry. This is basically it for the characters page. Let's go to the character, single one, and do kind of the same. I want to import getCharacter here. And I'm going to copy-paste the call and modify it slightly. Because for the character, we will need to pass an additional parameter to getCharacter, an ID. That's why I will also don't need isError here. The syntax slightly changes. First argument is an array now, where I pass props ID. And this comes from the router. And second will be a callback, where I call getCharacter. Oops. It was unexpected. With this props ID parameter. Here we go. And I will copy-paste my data value data from here. This is api. And I will replace character. So my character is this. And I don't need a slot in anymore. My character is computed based on data value data. And I click here. What happened? Oh, I forgot to import it computed. Here we go. And here is PoloTraders. And now the interesting moment. I go to characters. Characters are fetched. I go to Polo. Polo is not fetched. Characters, Polo. Characters, Polo. Because the data right now is in view query cache. But here goes cool thing. I have a defined stale time, which means that all my queries will be stale after the 20 seconds. So 20 seconds, we're hitting the cache. After 20 seconds, they are all invalidated and removed from the cache. And then I will have an api call. So right now, when I go to characters, I have a characters request. And I will go to Polo. Here goes Polo request. Of course, this is like super small part of what our view query can actually handle. But I hope it was an interesting moment to look at. And I hope you will dive a bit into this library and give it a try. Thank you all for attending the talk. Bye. Awesome. Thank you so much for that talk, Natalia. Let's have a quick look at the poll results just before we move to the Q&A. We discussed it a little bit up front. And I heard from you that you're not really surprised by the outcome, that most people said they're traditionalists and they use vuex. Why are you not surprised? Well, vuex is a mainstream thing. And I think for a long time, it was almost the only option. You could use Redux. But like 99% of people I knew were using vuex. What I'm more surprised is like, look how well it's doing the second option, right? Equinea. Congratulations to Eduardo, because you can see that Equinea is super popular right now comparing to vuex. It's overdoing Redux, MobX, XState, whatever we can have. And graphql. I mean, graphql depends on graphql. But I didn't expect these to be so high. Look also, I feel like some people want to be heard, because I see the percentages change. So it seems like people were like, wait, no, we're not a bunch of traditionalists. We're actually like really, you know, we're bleeding edge. Let's just give more voices to Equinea. Exactly. There's like a fan club that wants Equinea to do better. So I love that. So thanks, everyone, for joining into the poll. That's very cool. Oh, it's 20% now. Just look at this. Exactly. Like, can we just keep this slide up? I'm super curious where we're at at the end of the Q&A. I have a couple of questions for you, Natalia. One of them is, are you using vue Query in production right now yourself? I think nobody is quite using vue Query in production right now. It was super fresh. I think around like three months ago. I wonder if we even have versions of vue. Because I noticed that there was a core of react Query exposed. Before it was only a library for react, as you might guess from the name, like react Query. They exposed the core and said it's possible right now to build any kind of wrappers for the frameworks. Like, there is no, like, should I maybe try it myself? And then I figured out that there was a guy from Poland, Damian, already working on the library. He was working on his own before even starting communicating with react Query. But then he brought the prototype and they made it an official package. So it's super, super fresh. And I wouldn't recommend like jumping and pulling it straight to production. But it would be nice, I think, for people to experiment around it, to try the approach, to see if it's working for you or not. Because I believe the idea behind it is great. I was watching react Query for a long time. How it goes, how it works. And it's quite a big thing in react world right now. So hopefully maybe vue Query will be something in vue world similarly, like the react ecosystem. Interesting. Thank you. Let's jump to some of the other questions. One by Organized Chaos. Do you have any tips for deciding which data to use and when to use the server side cache versus to use local? Well, it's quite simple. If you fetch anything from the api, it's a server state. So it goes simply to vue Query or server state or whatever you how you want to separate it. And anything that you have on the front end side, on the client, goes to the local state. You might say, what if I need to mix two? So for example, if I have something like array of properties and I want to insert one more property to every single entity, right? If I have, for example, an array of books and I'm creating a list of favorite books, so maybe I should insert the property is favorite. In this particular case, I would say store books in the server cache like vue Query. Don't inject any properties to them. It's not the best practice, honestly, even if you're using just UX. Create a separate array of IDs, like favorite books IDs, and do the mapping on the component. You can always have any kind of a getter or computed property just to check if the book is in favorites, right? Without adding a property to the initial array you're fetching. It's something that I learned working a lot with apollo Client because apollo Client is super nice as well with handling server cache. And at first we were trying to, let's modify things in apollo Cache with inserting properties there. Like, no, no, it doesn't work this way. Whatever you want to add on the client, create a separate property and do the mapping after. There's a question by Jeroen Heijmans, how would this story change when the user interacts, it navigates heavily while requests are in progress or other real world, I love this, real world weirdness, like calls returning in an order other than they were sent, for instance. When would something like an extra library for sagas or something similar be needed, if at all? It's a great question. I really like the part about real life weirdness. Yes. For vue Query, it has a few advanced mechanisms for handling simultaneous requests and canceling them as well, which apparently is super good because it cannot do this neither with apollo Client nor properly with Axios if you're using vuex, but it handles queries super nice, like the duplicating if query is in progress. So even for real life weirdness, there are some advanced scenarios, unfortunately, I didn't have enough time to show it in practice, but if you check the documentation, and I would recommend checking the documentation of react Query in this particular case, because vue Query documentation is not full. People are working on it right now and adding, but if you check react Query for cancelling requests, it's there. Cool, I love one of the questions that is next, which is what font are you using in VS Code or what theme are you using? Apparently people are liking it. So it's a great question. I have it every single conference talk I'm doing, because people either like it or hate it, like absolutely, your ass character isn't readable, you cannot just even understand what you're typing. So it's love or hate theme. I'm using night all theme in VS Code by Sarah Dessner, and the font is Dank Mono. Okay, I see a lot of people switching up their VS Code environment right now. Let's see, another question. Is there a way to automatically refetch data after 20 seconds TTL and notify components? I think you can add the interval there. Again, there is an api in react Query, so you can do the same, I would probably call it polling. You can do the same thing with react and vue Query. So you can specify the polling interval there, and it will be polling. About notifying components, it's notified automatically because of the reactivity. So your components are already subscribed to whatever is in the cache. You can think about use query in your component like a getter. Whenever query is updated, component gets updated as well because it's reactive. And yeah, you can do polling as well. Very cool. While we wait for some more questions to come in, I totally want to touch on something and you know, satisfy my own curiosity. GitLab went public this week, I think. It was huge deals. So I think congratulations are in order. It was last week, I think it was Thursday the 14th. And we even had a day off after this. So you can see the celebration is real. Thank you. When public still cannot get used to this thing for a public company now. And it was quite fun to observe. I mean, we live streamed the listing process, like completely on NASDAQ. And it was kind of a madness when you're inside the company and watching everyone celebrating, wearing swag, being happy about it. So yeah. Awesome. So you had cake and a beverage of choice. Yes, we all did. And it went really good. That's very cool. Do you expect any changes to your daily, you know, your day to day? Oh, I think we kind of focused more not. Okay, there is one change. I can tell for sure that probably every single employee, including me right now, has a ticker from NASDAQ right now checking like stock option price, just because it's interesting. Yeah, we will get bored of it and stop checking the price. But right now it's very new and everyone's like, how are they doing there? And for the company processes, I think nothing has changed. We're still like a relaxed, remote company with all the cool stuff, like limited days off and so on and so on. There is nothing moving to corporate culture. Well, I think it's just super interesting and I'm totally going to continue following and also I also want a ticker just for fun. Awesome. I have a next question. It's going to be a quick one, though, because we're sort of running out of time. Which one do I want to choose? Your demo uses vue 3. Can we use vue query with vue 2 as well? Yes, we can. It works with the Composition api plugin. That was a quick answer to a quick question. So that's great. So thank you so much for staying on for the Q&A, for satisfying my curiosity as well. And so, yeah, thank you. For everyone here, please join Natalia in her speaker room on Spatial.chat if you want to continue asking her questions. The link to join is on the timeline at vue.jsLive.com. And please do let us know what you thought of Natalia's talk as well, once she left the stage, of course. Thank you for having me.
24 min
20 Oct, 2021

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Workshops on related topic