1. Introduction to Handling Local State
Hello, everyone! Today we are going to talk about handling the local state of our application when dealing with asynchronous behavior and API requests. I'm Natalia Tepluchina, a Vue.js core team member and Stack Frontend Engineer at GitLab. Let's dive into it!
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 are 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 Tepluchina. I'm a Vue.js core team member. I work mostly on Vue documentation, so if you are reading Vue 3 docs, there are high chances that you are 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.Net expert in Web Technologies.
2. Understanding Global State
Global state is data that is shared across components, views, and routes in our application. Let's take a quick example using Vuex 4. We have a local state with a property called count, which can be incremented using mutations. Some companies prefer wrapping mutations in actions for better factoring when dealing with asynchronous behavior.
So let's start diving into our global state. So what is 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 work 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 0. 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, like suggesting by name. And this is already enough to use this in our component, but some companies like my own one have 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 a sufficient boilerplate for incrementing count.
3. Handling Server Data and Asynchronous Behavior
In this example, we deal with synchronous changes in the state and don't require an action. However, when dealing with server data, adding a loading state and error handling becomes necessary. This complicates the simple structure of one-action-one-mutation, as we need to update loading and error states. Multiple requests may require different loading states, raising further questions.
What we are dealing with here in this particular example. First of all, we deal with synchronous 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. It has 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 a first look, it looks like the same count. We have a property characters, we have an imitation that changes characters, and we have some asynchronous action that fetches data from our character's endpoint. And after this, we commit a 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 are idle. You are waiting for your API to return your data. And of course, you need to add a loading state, because we need to show this nice loading spinner or skeleton or whatever you're showing in your application. And also, we are fetching data from the API. And what if the response is not there? What if your quest 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 Gateweb uses as well to handle asynchronous behavior with free quests. So here we have like 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 the 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 a character, and then we have characters, we commit the success mutation, storing the characters to the state, and then 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 yeah, it's huge for one single request, and 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.
4. Fetching Data Not in Vuex
How to fetch data not in Vuex? No getter exists for this purpose, so we have to implement the same behavior repeatedly in our application. It can be frustrating to check the state and dispatch actions every time. This is a common issue that requires re-implementation.
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. The getter is only a 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, okay, if this 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 XSELs, 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.
5. Keeping Data Up-to-Date in VueX
In VueX, keeping the data up-to-date can be challenging. WebSockets or polling can be used to update the VueX state. However, deduplicating requests is not built-in, resulting in multiple resolved requests. Asynchronous behavior in VueX poses challenges due to shared ownership and the need to reflect changes between server and client. Vuex is not specifically designed for this, similar to other state managers. Differentiating between global state, local state, and server cache is important. Let's consider an example of Vuex in action.
The second one is how to keep my data up to date. In this particular case, we are still thinking about VueX as a single source of truth, but it's not, because your source of truth is on the API. And the 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 tried just to have this reflection on our front-end. And we need to keep this reflection up-to-date.
If you're lucky, you have WebSockets, so on WebSocket event, you update your VueX state. For those of us who can't allow themselves WebSockets, we do polling. Every 30 seconds, we send a request to the server, triggering all these changes of actions, permutations, 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, and the action will be triggered twice.
Why is it all happening? Why do we have so many problems with asynchronous behaviour 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 persisted remotely, something that has a source of truth outside of our control as a frontend developer. And that's why it has 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 frontend 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 application, 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 a task, yep, 3001. Here we go. And I like Dune, I like the recent movies, so I built a fake API just fetching a few of Dune characters. And what we have here, we are fetching the array of characters on our main page, here it goes.
6. Handling Character Data and Fragile Frontend
And when I go to some of the route, I fetch one more character. All the characters are stored in Vuex. I have the same loading state for character and characters, and error state as well. I select a character from the characters array by index and enrich the data. This is super fragile. I check if the character already has a description, and if not, I dispatch fetch character. When fetching poll, I always fetch the character list first, even if I don't need it. An alternative to this is Apollo Client if you work with GraphQL and Vue 3.
And when I go to some of the route, 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're 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, so 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 and name. Every single character. And when I go to this route, I have a bunch of data, like description, section, when the character was born, and when I go back, I have, like, Paul and Channing, 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 poll, 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 poll, I always fetch this character list first. I don't even need it. I need only POLL or TRADE this. Why am I fetching all those? 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 a use query from your Apollo Composable.
7. Handling API Calls and Local State
Whenever you call the API, the query will first hit the cache. Apollo Client can also work with REST API using a resolver. However, handling local state with Apollo Client can be more complicated than using Vuex. There is no one-size-fits-all solution. React Query is a great tool for handling local state in React, and now there is a Vue query wrapper for Vue as well.
And whenever you call the API, because this will trigger an API call on 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. 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 cache first.
If you wonder what is charactersQueryGQL, if you 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 a local state in Apollo Client. So imagine if you want to add a character. You would need to do all of this. You will read the query from the cache, creating your character, creating your DataObject and you, because Apollo Cache is immutable, and then you write a 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 in handling local state and is 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 CraftQL API. And it's a bit more questionable when we need to deal with local state. Maybe our desire to have a single source of truth is not as great. Maybe we have to 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.
8. Using useQuery to Fetch Data
In our Vue application, we import the useQuery method from the Vue query library and the getCharacters method from api.js. We use useQuery to fetch data and return it from the setup. The data contains an array of 10 characters, which we can use to create a computed property. We also expose the isLoading and isError states in our app. The useQuery function handles loading and error states, and we can implement retries for connectivity issues.
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 Axios 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. So 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 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 is loading and is error here in my app, instead of creating these two refs. And right now there is super short moment when you can see loading here blinking. And if I make a mistake in my API. And after this, it will say, okay, 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.
9. Using view query for character data
Let's import getCharacter and modify the code slightly to pass an additional parameter, id. The character is computed based on data value data. Characters are fetched from the view query cache, and after 20 seconds, they're invalidated and removed. This is just a small part of what view query can handle. Give it a try!
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 code 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 delete isError here. The syntax slightly changes. The 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 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 as loading 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 Paul Atreides.
And now the interesting moment, I go to characters, characters are fetched. I go to Paul, Paul is not fetched. Characters, all. Characters, all 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're 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 go to Paul, here goes Paul 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.
10. Poll Results and Vue Query
Let's have a quick look at the poll results. VueX is a mainstream thing and for a long time it was almost the only option. Pinnia is super popular right now, overdoing Redux, MobX, XState, and even GraphQL. It's great to see people embracing new options. Thanks everyone for joining the poll. Vue Query is a fresh library that allows building wrappers for different frameworks. It's still in the early stages, but worth experimenting with.
Awesome. Thank you so much for this talk, Natalia. Let's have a quick look at the poll results just before we move to the Q&A. We discussed a little bit upfront. 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 99% of people I knew were using VueX. What I'm more surprised is how well it's doing the second option. Congratulations to Eduardo because you can see that Pinnia is super popular right now comparing to VueX. It's overdoing Redux, MobX, XState, whatever we can have. I mean, GraphQL depends on GraphQL, but I didn't expect these to be so high. I feel like some people want to 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 are we're actually like, really, you know, we're bleeding edge. Let's just give more voices to Pinnia. Exactly there is like a fan club that wants Pinnia to do better. So I love that. So thanks everyone for joining into the poll. That's very cool. Yeah, that's 20% now, just look. 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, 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 a prototype and they made it an official package. It's super, super fresh and I wouldn't recommend 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.
11. React Query, Vue Query, and Data Handling
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. 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. Anything that you have on the frontend side of the client goes to the local state. If you need to mix the two, create a separate property and do the mapping after.
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, how you want to separate it. And anything that you have on the frontend side of 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 an 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. 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 Vue. Create a separate array of ideas like favorite books, ideas, and with the mapping on the component. You can always have any kind of a getter or computed property just to check if the book favorites 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. At first, we were trying to modify things in Apollo Cache with inserting properties there. Nope, nope. It doesn't work this way. Whatever you want to add on the client, create a separate property and do the mapping after.
12. Handling Simultaneous Requests and Cancellation
For Vue Query, it has advanced mechanisms for handling simultaneous requests and cancelling them. It handles queries nicely, even in real-life weird scenarios. Check the React Query documentation for cancelling requests.
There's a question by Jeroen Heijmans. How would the story change when the user interacts and 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 cancelling 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 a query is in progress. So even for real life weirdness, there are some advanced scenarios. Because 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.
13. VSCode Theme and Font
I'm using night all theme in VSCode by Sarah Dresner. And the font is bank mono.
Cool. Cool. I love one of the questions that is next, which is, what font are you using in VSCode? 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, like, your 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 VSCode by Sarah Dresner. And the font is bank mono. Okay. I see a lot of people switching up their VSCode environment right now.
14. Automated Data Refetch and Component Notification
Is there a way to automatically refetch data after 20 seconds TTL and notify components? Yes, you can achieve this using the interval option in React Query. By specifying the polling interval in your query, the data will be automatically refetched. The components are notified automatically due to reactivity, as they are subscribed to the cache. This allows the components to update whenever the query is updated. Polling is a cool feature that can be implemented.
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 on React query. So you can do the same. I would probably call it polling. You can do the same thing with React in your query. So you can specify it the polling interval there. And it will be polled. 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 your query in your component like a getter. Whenever a query is updated, the component gets updated as well. Because it's reactive. And yeah, you can do polling as well. Very cool.
15. GitLab Going Public
GitLab recently went public, and it was a huge deal. The company celebrated with a live stream of the listing process on NASDAQ, and the atmosphere was filled with excitement and happiness. It's still a new experience for us to be a public company, and we're all adjusting to this new reality.
While we wait for some more questions to come in, I totally want to touch on something and 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. We even had a day off after this. So you can see the celebration is real. Thank you. We went 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.
16. GitLab Going Public Celebration
We had cake and beverages to celebrate GitLab going public. The only change is that employees now have tickers to check stock option prices. Company processes remain the same, and we're still a relaxed remote company. It's super interesting, and I'm excited to continue following.
Awesome. So you had cake and a beverage of choice? Yes. We all did. Eats went really good. That's very cool. Do you expect any changes to your daily, you know, your day to day? I think we kind of focused more on 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 prices because it's interesting. Probably we will get bored of it and stop checking the price, but right now it's very new and it's like, how they're doing there. So, and for the company processes, I think nothing has changed. We're still like 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 gonna continue following and I also want a ticker, just for funsies.