When we think about Vuex, Pinia, or stores in general we often think about state management and the Flux patterns but not only do stores not always follow the Flux pattern, there is so much more about stores that make them worth using! Plugins, Devtools, server-side rendering, TypeScript integrations... Let's dive into everything beyond state management with Pinia with practical examples about plugins and Devtools to get the most out of your stores.
Hi everyone, my name is Eduardo. I live in Paris. I'm a front-end developer and also a member of the Vue.js core team. I'm the author of its structure, and also a small library called, Pinia, which is an alternative to Vuex, a store library, as well, that I'm going to talk about today.
What is State Management?
And what I want to talk to you about is not only Pinia, but what is beyond state management, what matters in state management, more than the state itself. But first, before even diving into Pinia, or anything else, what is state management? Is just having a global object that you change with a few functions. Just like that, global state, have a user. Then you have a function login, that allows you to change the state. Is this state management? And people will tell you that it is. And to be honest, it pretty much is, especially if you just add a reactive function call around that object. Well, you have a completely valued global state solution working for your Vue 3 or Vue 2 application.
[01:26] And this is still state management. And some people will tell you, but state management is something you have when you create, when you have bigger apps or medium to big apps, or when you know your application is going to grow and is going to need that complexity in the future. But this is pretty much... You don't need to have a complex application to need or consider using a state management solution, or a store.
What defines if you should or not use a state management solution is how the store is used inside of your application. For example, is your state used in many different places in pages, or does your state outlive pages? Do you need your state when you go from one page or another? Maybe your application only has three pages, but you require that state to outlive all the pages. And in that scenario, you need a state management.
Do you need a Store?
[02:27] And then the next question that comes to my mind is, should I stop having that bare bones, reactive global state management solution? Or should I use a store? And the truth is, there is a very small step to use a store. Just by using defineStore with Pinia or Vuex 5, you can pretty much keep the same things that you were having. The state is going to be in a pretty cool state, then you're going to have... The global state variable is going to become "this". So, "this.user = something."
So, the chain is very simple. And the cost in terms of size, is also very small, it's like 1.5 kilobytes for Pinia. But of course it comes with many other things, right? So, do you really need a store? What makes you change your mind? What makes you go from, "Okay, I'm going to stop using my bare solution and start using a store." And there are a few reasons that will make you go the direction. The main reason is server-side rendering is definitely so much easier to handle with a store like Pinia or Vuex. But there are many more.
[03:42] So, you also have state outliving components. This pretty much comes within the box for the state management solutions as well, but depending on how and where you want to instantiate the global state. Is not something that always comes in. And then you have the centralized state, shared by many components. So, if you create one single global state, that's not going to scale forever. If you keep adding things, new functions, new properties to your state, at some point that state and that fire's going to get bigger. You can explain with multiple files as well, but you're going to make your object grow in complexity way too much. And that's going to create all the problems, if you watch the state or do any other complicated stuff.
Then you also have the better developer experience. So, if you are used to using a global object, you cannot inspect, change that state within that tool somewhere. You have to do it manually through the component inspector. You don't have anything in the timeline. You don't know where the changes to the store, to the state, sorry, comes from. So, if you want to keep your state while you are changing the store or your state management solution, it's going to reload the page maybe for... You will have to handle hot module replacement yourself. Then if you want to test or unit test your components, you are going to need to mock your state management solution or test it twice. So, have test for your state management and also test that rely on the behavior, core behavior of the store in the component.
[05:26] And then if you want to handle local storage or undo/redo or any other functionality that is more generic and that applies to any store, any state management solution, you're going to have to implement that by yourself. Whereas if you have a store, all these things come out of the box, you don't need to do anything. So, this is why I want to talk to you a bit about how do we achieve these things and what are these things that exist when you use something, a store Pinia and make you consider using one?
What is Pinia?
[05:57] So, first of all, what is Pinia? If you take this main .JS file or .TS file that you have in your project, you will see that you have this app, create app, and to add Pinia, you only need to import this, "create Pinia from Pinia" and then create what you call a Pinia and use it.
And this is a very important part of using Pinia, because that object that you have is the registry of all the store, the centralized state. That means that when you create Pinia, there is a state property on that object that is pretty much an empty reactive object. And later on, whenever you call your store for the first time, that store is going to be created. Its state is going to get added dynamically to that empty object that we created. And the same happens with any other store that we call after that. Okay?
[07:00] So, that Pinia state is going to grow as the user keeps using the application as you need. And having this centralized state has been a core concept in Pinia at the very beginning, which was about two years ago already. And that was even one month before Vue 3 even got released for the first time.
So, it means that when Pinia existed... The first time Pinia... The first version of Pinia was only compatible with Vue 2, because there was no Vue 3 or whatever. And at that time, things were very different from now. I was calling internal functions of the Devtools v5. So, that was pretty much just hacking around the non-public API of the Devtools, so I could display limitations and the state in the Devtools where Vuex was supposed to display its information. I would have to duplicate getters and actions every time we call Vue store. So, that will create a larger memory footprint and also worse performance when it comes to computer properties.
[08:07] There was no extensibility or whatsoever. There was no plugin interface at all. And there was only an option-based API. Today, with Pinia 2, we have support for Vue 2 and Vue 3. We have two different syntax we can use. We have the option API, and we have the setup syntax, which we're going to see later. And we have very advanced Devtools support for the version six. So, we have a timeline we can inspect per component. We can inspect all the stores. We can modify them. We have hardware replacement with a very simple one-liner for your stores. We have a testing module that allows you to pretty much create testing Pinia that mocks any store that is used by your components without you having to change anything in your components. You have a Nuxt module that works for Nuxt 2 and Nuxt 3. And you have type safe extensibility with plugins, which I won't dive into, but basically, you can add any option to your stores and then add properties to the store and they can be typed safely. And you can use the id of the store, the state of the store, the actions, anything you want. It's pretty powerful.
And another thing about Pinia, in this state that is important to mention is that the core API, the way you define stores is the same as Vuex 5. So, Vuex 5 came after Pinia and it has influenced a lot, the process and the API of Vuex 5. And I keep a very close eye, because I participate a lot in this discussion. So, I make sure that anything that gets added to Vuex 5, it's added to Pinia in a way that is very easy, in the future, to change from one or to the other, especially for the core of it.
[10:04] And the main changes of Vuex 5 and Pinia compared to Vuex 4, and the thing that most people will notice is that we no longer have mutations. Overall, we have a much less verbose version of the stores. The mutations completely disappear. So, you have instead the state, and then you can notate the state directly on the store, but that's not the only way, at least for Pinia, to modify the state. We can also use a function patch that allows us to pass partial object, a patch object that we apply to the store. And we can also pass a function so we can modify the state directly. This is useful when we are interacting with collections like sets, maps or arrays.
Then we have the one-liner hot module replacement, as I was saying before, which is pretty much just a line you copy every time in the stores. And you forget about it. And then you have the plugins. The plugins in Pinia, I try to give them as much power as possible. As a matter of fact, the Devtools, the plugin... I mean, the Devtools implementation for Pinia is a Pinia plugin.
[11:24] And you can do many things. The plugins for Pinia are just functions and they receive the application, the Pinia instance, the store and its options, the options with which the store was defined. And usually you do three different, possible things. You can either call a subscribe function that is going to trigger every time you mutate the state of your store, but it also accounts for patches. So, if you do a patch with a function or with an object, it's only going to trigger one differently from a watch. Then we have onAction, which triggers before an action executes and allows you to control the outcome, and if there is an error, or if it succeeds, you can do all the things.
And then the most simple, but yet, so powerful functionality is to return an object on the plugins. And these objects that you return in the plugins are going to get merged into the store. See, so this allows you to override or add new properties to every single store. You can also filter by ID if you want. You can also read from the options. You can do many things. And this is a very simple example, is that you can just add the router to every single store. And then in the actions, you can just call 'this.router'. And that's it. You get the router, you can push, you can replace, you can check the current route if you want. But, one of my favorite ones is the onAction. The onAction looks very simple, and it is very simple for a lot of cases, but it's very powerful.
[13:02] So, this is what is used by the Devtools behind the scenes to display the timeline, which we're going to see just in a moment. But for example, this is the code you need to send errors from your actions to a reporting service, like Sentry or anything. So, you just go onAction, and then onError, you get the error and you send the errors with the name of the action that failed. You can also send the store ID, which I forgot to put there. And then the arguments that were passed.
And I want to show you to... There is much more, like the Nuxt plugin and the Pinia testing module that I want to show you, but I want you to give you a taste of using Pinia, what it looks like with all this functionality. So, let me show you.
[13:59] Okay. So, I have a server running. All right. So, I have a Vite server running here with my application. And I have one page, very simple page where I have counter demo one. This is just... As you can see… There we go. So, we have this up and running.
The demo counter. I have a use counter that I'm importing from this file right here, and I'm just using the counter and I'm directly calling 'counter.n++', and I'm displaying the whole state of the counter. And so, the demo counter, what I have is just a defined store. I have my initial state here and I have the hot module replacement snippet of code. And what I wanted to show you is, here, I can increment the state, but I can also add new properties. Okay. And if I add a new property, it will appear, it will leave the old state, but it will add itself to the state of the store. And if I remove it, it's going to disappear from the state as well.
[15:13] So, of course this works with everything else. The getters, the actions. And I want to show you a little bit of the Devtools that we have and how cool it is to work with the store, and debug your applications, thanks to the Devtools. So... Here I have an increment. I can increment my state. And if I open the Devtools here, I can inspect the counter and I can see here that I have my state, I can implement it, I can change it if I want. Okay. And here I have a tan line. And if I go down, I can see that I have Pinia. And if I increment, I can see small dots, yellow dots. And if I select them, there we go, we can see three different dots that we have, which are here, and what I'm doing. So, here mutation goes from 102 to 103 on the key and to the counter.
So, what I'm going to do, I'm going to add an action here. I'm going to add an action that is going to decrement, decrease until it gets to zero. This is going to be async action. And I'm going to do while this.n bigger than zero I'm going to decrement and then I'm going to await a small delay of time of 200 milliseconds.
[16:50] So, that's it. That is the action. It's very simple. Now what I'm going to do is, at the bottom here, I'm going to call until dot decrease to zero. And we have auto completion as well, pretty cool. Decrease to zero. Just change the text a little bit. All right.
So, here we're just doing 200, because it's faster than waiting one second. And what we want to see is how the changes of N can be inspected one by one in the Devtools. So, here, let me just show you the timeline. We're going to clear. And again, to decrease to zero.
[17:39] And as you can see here, they start appearing. You can see how N starts decreasing. And what's interesting, I can also call increment, and they appear right under, and I can also have multiple actions running in parallel, as you can see until N gets to zero. And then we can inspect the actions and inspect all the limitations by group.
So, here, if I switch to group, I can see that this action here, we have a total of 74 events and it took 40 seconds. 14 seconds, sorry, to finish. The other ones are a bit shorter. So, we have less events and they took less long. And every time we can see the new value and the old value. So, that's pretty cool. And what's interesting is, we can also go to the state management here, get to the actual state, so we can see all the state. We can change it from there. And we can do that while the action is happening. We can modify that anytime. So here, if I modify the value and I go to 50, you can see that it changed right away here. We don't need to do anything else. And in the timeline, we have the events happening at the same time and they get logged in.
[18:58] Furthermore, if we want to group multiple modifications, let's say we want to count how many times we decrease. And so, this means that... Decrease times implement. So, it appears here. We can increment a few times. We decreased to zero. And now if I check the Devtools, I can see that I have more mutations. I have one for changing N here. And one for changing decrease times, which has a type by the way. Let me see. There you go.
So, one thing I can do instead is using the platform component. So, we do this state... Oops. And then here we can do what we want. And this will group the modification. So, here let's run a few times and decrement. Now we go check this and we can see that we have less events. We can inspect and see the old values and the new values. Five and zero for N, that was the last one. First one, five and four.
[20:33] And one thing that's very interesting, and lot of people don't know is that inside of state, we can call any compostable function that we have. We can call, watch. We can call compete, anything we want.
So, if I want to save the state to the local storage, I can just use a Vue core function, use local storage, and I can just replace, use local storage, counter demo. I can give you an initial value of zero. So, I can use that. I'm going to reload the page, because I just changed the nature of these state properties. I need to probably reload the page. We have a zero. If we increment here and I reload the page, we can see that the eight is the same. It hasn't changed. And everything else works the same.
[21:26] So, very simple yet so powerful. You can use any composition API right in here, and anything else works the same. Right? It's just a state, so you can still inspect it here. You can still change it and it will adapt.
All right. So, this is the option version of a store. And so, you have a state, you have the actions, you have the getters as well, and you can add all the properties to the options if you want. For example, you can add the bounce option that allows you to define which actions you want to bounce, but there is another reversion of the syntax that we can use. And that's the setup syntax.
[22:20] So, the setup syntax, I have a different example for that. It's a bit longer. I cannot go through the whole thing. But instead of having an object like we have here, we have a function. This function is the same as the setup function on component. And you just create the same as in a component, res computed functions, you use your composables the way you want. For example, this is a use cache request. So, it's similar to state evaluate. It caches the first time and it still does the request in the background. So, a very, very simple one. And then we have a few functions. And we can even do Watchers. So, that's pretty cool too. Here, for example, if I go to the... And then I get a few times, I get a few functions, a few images. And then when I go back, I can see that I get the image right away and, in the background, is doing a call, anyway. And here, you can see that this store appear. We still have the demo counter, which hasn't changed. But as soon as we got into this page, we got access to this new store. As you can see here, we can see how it changes from one day to the other. We can also have the errors by the way, which I didn't show. But I don't think I have it implemented here to show you.
We can see the errors in the timeline also. And we can inspect them right away in the Devtools, which is pretty cool. We can log them as well. So, we don't even need to add console logs anywhere, because we can do that on demand through the Devtools. So, you can focus on adding just the debugger or break points, once you want to debug that. But you can just console log from the Devtools anything you want. Usually have, just to show you... Even though here, it is not very important, but here you can log it to the console. And so, that give you access to the actual error, where it comes from.
[24:22] So, that's it. That's everything I wanted to show you for the demo. I want to finish by saying thank you to the sponsors I have on... The GitHub sponsors, who allow me to work most of the time on open source. So, if you like what I do on the router, on Vuex, on Pinia, whatever, please consider supporting me, especially if you're a company. And there are nice perks by the way, in doing that. And I also want to thank you for listening to me. I hope you learned something and that you're going to give Pinia a try soon.
[24:58] Ari Clark: Let's look at the results from the poll. Now, since this one was... There was a technically correct answer to this. Was that right?
Eduardo San Martin Morote: Yeah. Yeah. There is one correct answer.
[25:09] Ari Clark: And which one was the correct answer?
[25:14] Eduardo San Martin Morote: So, the correct answer is the least voted one. It's a fun fact about fruits, more specifically, the pineapple fruit. The other ones were the trap. The manga was clearly a big trap. I don't even know any pineapple gangster manga, but I thought that was funny.
[25:24] Ari Clark: I wanted to vote for that just because it sounded fun. Okay. So, what's the fun fact?
[25:42] Eduardo San Martin Morote: So, it's written in the docs. A pineapple is actually a multi fruit. So, it flourishes as multiple flowers, and then you have multiple fruits growing out of the flowers, and then they fusion together to become one big fruit, which is the pineapple. So, this is pretty much like stores. You create stores individually. And at the end, they gather together into one big centralized store. Which is my Pinia, basically.
[26:07] Ari Clark: Very cool. Okay. I guess it's time to ask questions. The first question comes from Organized Chaos. What is the pattern for global store usage? And also, does Pinia suggest splitting out stores for different component tracks like Vuex? We're assuming component tracks here means just feature related components? If we're wrong about that Organized Chaos, sorry.
[26:38] Eduardo San Martin Morote: Trying to find the question, so I can read it. Okay. There we go. I cannot find it. So, there is not really use fleet. You create a store for a feature. So, as I say, in the talk, you usually put in the store information that outlives pages, or that requires some kind of... Or it's just use all across application. So, it doesn't have necessarily to outlive a page. Sometimes, it's also easier to put the business logic in a store. So, that could be a specific feature, but especially with the composition API, that's really not necessary. So, sometimes for some specific business logic, it might be useful, especially if it's used across the application, like authentication or in some eCommerce websites the cart can get very complicated.
The reason... So, ideally with Pinia, you create one file per store. So, you don't create multiple stores in one file. And that's it. There is nothing else. Then you're just free to do whatever you want, to import them, whatever you want, as long as they are used inside the set of functions or you pass the Pinia instance as shown in the documentation.
[28:06] Ari Clark: Okay. Next question comes from VRex. When updating the state of tens to hundreds of components in real time, as reaction to a user input or for animation, we noticed Vuex is extremely slow. How is the performance of Pinia compared to Vuex?
[28:28] Eduardo San Martin Morote: So, I don't... So, I think the person is talking about Vuex 4 or Vuex 3, probably. I don't think Vuex has anything to deal with the slowness in these cases, in these scenarios. So, Pinia shouldn't change that. Maybe Pinia is faster than Vuex 4 and Vuex 5 will be faster than Vuex 4 as well. But I don't think that, especially animation that are slow. Yeah. It's probably not about data. It's more about the properties animated and maybe some Watchers are synced, it's just impossible to know without an actual example.
[29:12] Ari Clark: Okay. Next question comes from Ricky Yu. I can change state directly in Vuex already. I just bypass mutations. What's wrong with that besides everyone telling me it's wrong to do?
[29:27] Eduardo San Martin Morote: You won't get the Devtools experience. You are not able to see limitations or anything in the Devtools. So, that's a big impact in DX. That's it.
Ari Clark: That's it? Okay. So, really we could just be doing that.
[29:54] Eduardo San Martin Morote: I mean, things could go wrong also, but you're not... It has been two years since I haven't used classic augmentation based on Vuex. So, I don't remember if there are other things that could break, probably. Plugins won't work, you have all the plugins that won't react. So, it's going against the way of doing things. So, things are going to break.
Ari Clark: That makes sense.
Eduardo San Martin Morote: That's what's wrong.
[30:26] Ari Clark: Okay. Also from Ricky, you summarize Pinia versus Vuex 5 top three reasons to migrate. So, give us your elevator pitch.
[30:41] Eduardo San Martin Morote: Yeah. I don't think the Vuex 5 is there yet, anyway. So, there is nothing to compare to. There is an experimental branch. So, imagine that there was one and that it has implemented the API that is in the RFC. So, I'm pretty sure Kia has already a working version. We've seen it together.
But things that differentiate today Pinia from Vuex 5 is the plugin API, which is existing and more advanced. The Devtools experience, which is also more advanced as well. I'm not sure what Devtools experience exists for Vuex 5. The testing module. So, there is an official testing module for Pinia. I actually also did the Vuex version. And there is a Nuxt plugin as well. And it has a cuter logo.
Ari Clark: That's a huge selling point. I will say.
[31:39] Eduardo San Martin Morote: No, the real reasons, plugins and dev tools, I would say. If you want a third, that would be the tech support. Wait, if you actually have the same tech support... What was the other thing? The testing, I will say.
[31:56] Ari Clark: Okay, next question comes from Mr. Backend. How is Pinia only one kilobyte?
[32:04] Eduardo San Martin Morote: Because it's very simple. Actually the biggest part is the Devtools. It's 1.5 right now.
Ari Clark: Yeah, let's clarify, it's 1.5.
[32:17] Eduardo San Martin Morote:Yeah. Technically, it's not one anymore, right? It used to be one, but as I kept adding features, like the plugins and both the setup and the option syntax, it's now 1.5. Still very, very small, anyway. It doesn't do that much. What can I say? Because everything is dev... There is a big focus on development as well, experience. So, everything that is development experience gets stripped out in production.
[32:51] Ari Clark: Great. Very cool. Well, it is about time for us. So, thank you so much for joining us Eduardo.
Eduardo San Martin Morote: Thank you.