Proven Pinia Patterns

Bookmark

With Vue's new-and-improved state management library, Pinia, we gain a much more modular tool. While being more flexible, leaner, and lacking the Mutations of Vuex, Pinia presents us with more opportunities to be creative, for better or worse, with our app architecture and how state management is conducted and organized within it.

This talk explores some @posva-approved best practices and architectural design patterns to consider when using Pinia in production.



Transcription


vue. My name is Adam Jarr. I am co-founder of vue Mastery, the ultimate learning resource for vue developers. Through our collection of vue courses, you can level up your skills, elevate your code, and become the best vue developer you possibly can be. So in today's talk, we're going to take a journey deep into the vue ecosystem to explore the planet of Pinnia. As we should all be aware by now, Pinnia is the evolution of where vuex was, bringing a newer, lighter weight, more modular, less prescriptive, more freeing version of state management into our vue apps. And as the saying goes, with great freedom comes great responsibility. And we have added responsibility now because we can get creative about how we implement state management in our vue apps. And when we do this well, we can use Pinnia to develop very elegantly architected apps, avoiding anti-patterns, and creating an application whose state can scale as we scale. And that brings us to the focus of today's talk, where we unpack the proven Pinnia patterns that you can use confidently throughout your apps, because this talk and its contents were approved by Posva, or Eduardo San Martin Marote, the creator of Pinnia. So by the end of this talk, we will have covered organizing our Pinnia stores, options versus setup stores, how they differ, which one you might want to choose for what use case, accessing and updating your Pinnia state. And we'll also explore some unique Pinnia features, such as patch and reset. Throughout this talk, we're going to be looking at a demo app so we can apply and unpack these features in a more real world use case. And as you can see, this is a restaurant finder. You're going to type in the city and search Sherm to find restaurants within a certain area, and the user can register an account. They can save their favorites, and they can read information pulled from the Google Maps api, such as ratings and reviews. This app is going to be tracking each of its logical concerns globally with its own Pinnia stores. And we're going to cover all this throughout the talk. So let's get started. Before we dive deeper into Pinnia concepts, let's first get clear on why you would actually want to use Pinnia in the first place, because vue 3 already has the composition api with a built in reactivity system, with the flexibility for sharing and reusing state. Just using the composition api, we could create a reactive object to serve as a store to manage our global state. Then we would just import that store into whichever component needs it, and because of how vue's reactivity system works, any component that imports that store can directly mutate its global state. And if that state were to change and it had a template that were displaying that state, that template would reactively change as well. So, again, if we can just use the composition api to create a minimal, fundamental version of state management in a vue 3 app, then when exactly would we need a state management library such as Pinnia? Well, it's going to come in handy when you want to have consistent patterns for collaborative organization. This is especially important for large teams that are collaborating on a large scale application. One such pattern that you might want to follow collaboratively together is a predictable way for you to mutate state, and the actions within Pinnia allow us to achieve that. ssr security is another thing to consider and why you might want to use Pinnia, because when you're using server-side rendering, you'll need to be careful with how you manage global state, since ssr apps, they initialize the application modules on the server and then share that state across every request. So, this could lead to security vulnerabilities, but Pinnia was designed to make it safer and easier to manage state in those server-side rendered applications. Additionally, when using Pinnia, we get the dev tools and all the transparency and helpful insight that those tools can provide us. This is going to make debugging and really understanding the state of our applications a lot clearer. Increasingly, it's becoming important for javascript developers to be working with tools that have typescript support, and Pinnia has first-class support for using typescript. All of these reasons combined create for a smooth and intuitive developer experience when you're using Pinnia to manage state in your applications. So, if your needs include any of the things that I laid out in this list here, you're probably going to want to decide to use Pinnia instead of just relying on the Composition api for your state management needs. So, all of that to say, when you use Pinnia, you're going to be empowered with a robust and refined way to manage the state in your applications, architect it in a way that you can be able to debug and collaborate nicely amongst your team. So, now that we're clear on when exactly we would want to use Pinnia, let's start to explore those features. When you're using vue 3, you can choose to use the Options api or the Composition api. Similarly, when you're using Pinnia, you can define your stores as options or setup stores. Setup is going to refer to the setup function within the Composition api version of a Pinnia store. And there's really no right or wrong answer for which one you should be using in your apps, but let's explore the ways that they differ so that you can see which one might be better for your specific use case. First, let's review how we define each type of Pinnia store. Anytime we want to create a store, whether it's an option store or a setup store, we must first import define store from Pinnia. Then, we pass in a string for the name of the store as the first parameter. And this string needs to be unique because this is what Pinnia is going to be using to track the state of your store and it's also what you're going to see in the dev tools. It's also going to be helpful when you start to use plugins in Pinnia, so just be very mindful to have a unique and nice, understandable name for each of your stores as you're defining them. Now, let's take a look at the structure of an option store. Not coincidentally, this kind of store takes in an object as the second parameter of the define store function. The state, actions, and getters here are really analogous to the data, methods, and computer properties when you're writing a component with the Options api. Alternatively, with a setup store, the second parameter that you're feeding into define store, that is going to be a function. This kind of store will feel familiar to how you'd compose a component with the Composition api. We create state with ref or reactive. Our action is simply a function and our getter is a computed property. And note how we are returning everything here that we want to be accessible to other components. This is also going to make everything here that is returned trackable by the dev tools, so just make sure you don't forget to do that. Ultimately, like I mentioned earlier, whether you use options stores, setup stores, or options and setup stores in your same application, that is going to be up to you and your team. But I did want to explore some of the ways that setup stores do have advantages. Since setup stores allow us to take advantage of the vue 3 reactivity system, that's going to mean we can easily use composables in those stores, like composables from the vue use library. And we can also use watchers easily in them as well. We actually use both of those in the demo app that I referenced in the beginning of the talk. So let's take a look at that. Notice this behavior here where if the user doesn't type anything into the city input, their current location will be found automatically. Looking at the code inside of a store within this project that is called geolocation, we can see that it is indeed a setup store and its functionality relies on the use geolocation composable from vue use. It's using that to get the coordinates of our user. And then meanwhile, the watcher is keeping an eye on those coordinates to trigger an action to fetch the location data from the Google Maps api. So again, we're able to use both that composable and the watcher within this setup store. Now, it's important to understand that Pina is modular and it's modular by design. That means that we are encouraged by its very api to split up our state into manageable domains. This compares to vue X where we had one root store file and then modules that broke off from that. With Pina, we create stores that are devoted to each major logical concern of our app and then we can import those stores wherever they're needed. And this is going to help with bundlers for code splitting. It's going to give us better type inferences if we're using typescript. It's going to boost our team collaboration and it's going to add clarity for debugging. However, when it comes to actually putting this modularity into practice, it's not always so clear where and how you should be splitting up those stores based on the topic or the domain that they are concerned about. So that brings us to the topic of store organization or as I like to call it, store organization. Let's take a look at our example app to see a straightforward, kind of obvious way that we would separate out our stores. And then we're going to look at one that is a little more complex and nuanced. In the example app, our user can register an account and the user interface is going to change depending on whether a user is logged in. So if they are, we're going to see their name and the favorites showing up in the nav bar. So to achieve this authentication behavior, we need to keep track of the user state and also the actions related to that state in terms of registration, login, and log out. So it's pretty obvious that this can all be bundled into one store and we might call that the auth or authentication store. But not every store is going to be that intuitive with how you should be organizing the contents of it. So let's look at a little less obvious way that we would be store organizing within our PINIA apps. Remember how in our restaurants app we have these two inputs. The first input takes in the city. The second takes in the search term to find relevant local restaurants that are nearby that city. Both inputs have event listeners that trigger a function when the user types in text. Each function makes a call to the Google Maps api. Specifically, the city input is going to call for the latitude and longitude of the location typed in by the user. And then search is going to make a call for the relevant restaurants within a certain distance of that city. In other words, both of the city and the search inputs are utilizing geolocation specifically with the Google Maps api. So should we combine this into one store? Maybe we would call it Google Maps. Well, when we look at our project holistically, there are quite a few actions that are going to rely on the Google Maps api. So that would be a lot of code that we're cramming into one store. And more importantly, the actions aren't all going to be related to the same kind of logical concerns. So to get clear on how this should all be store organized, we can break it down to focus more on what is the Google Maps api actually being used for and what state do we need to be tracking? When we ask those questions, we know that the first input is being used to make geolocation requests, while the second one is dealing with restaurants that match the search term. So, yes, those restaurants are nearby. So there's an aspect of location involved with restaurants. But it's not its primary focus like the other input is that's actually making those geolocation requests. So based on this reasoning, it sounds like we can make two separate stores, each with their shared logical concerns, geolocation and restaurants. But you might be thinking, well, in the restaurant store, when we request that list of relevant restaurants, it requires the use of geolocation data. So this means we need to use the data from the geolocation store inside of the restaurant store. Does that fact just completely ruin our approach here? Well, no, it actually doesn't, because that brings us to another point within Pinnia, which is called nested stores. With nested stores, we can have a store and then nest another store inside of it based upon what its parent in this context would have needed. So we're making good progress in understanding some of the Pinnia features. So some key takeaways so far to take note of are that we should be grouping related data into their own stores by logical concern. We can also organize our stores by features within our app. We don't necessarily want to assume that a store should be created around one api or one library, and we can cross-share state amongst and between stores by nesting those stores inside of each other. Now that we've covered all that, let's look at the topic of how do we access our state within Pinnia. While other state management libraries can be very prescriptive and really force developers to be accessing and mutating their state in a specific way, this can lead to unnecessary overhead and some rigidity that can feel limiting. But one of the ways that Pinnia provides flexibility is by allowing developers to make their own choices about the patterns that they're using. So let's take a look at how we can access state within Pinnia and the options we have for doing that. If we're in a store itself, we have access to the state properties inside of our actions and getters. But there are some considerations to keep in mind as things are a bit different within an options versus a setup store. In an options store, if we're in an action and we need to be accessing state, we can do so using this. Within a getter in an options store, we're going to need to pass in the state to that getter to be able to access it. Now, on the other hand, in a setup store, for both actions and getters, we access the state property directly, just as we would within the setup function in a component that's using the composition api. In this example, the state property is a ref, so it's written as city.value. And of course, we need to remember to return everything so it can be accessible outside of the store. Now, what about accessing Pinnia state from inside of a component? Now, there's going to be a few ways to do this, the most common of which is to import the store into the view component itself and then invoke the useStore function. This is going to allow us to read the state and write to that state using dot notation. However, using dot notation could become burdensome if we're using a lot of different state properties within a component. So there's a more efficient way that we can actually do this. We can make our lives a lot easier by destructuring the properties from the store, so we're not having to write that store name all the time that we're accessing and writing and reading to it. But we do have to be careful about how we do this. Our first instinct might be to do something like this, but this is actually going to be breaking reactivity. In view3, we can't destructure props unless we use a helper called toRefs. You might be already familiar with this. And in Pinnia, similarly, we can't destructure state properties unless we use a Pinnia helper called storeToRefs. And this will keep the reactivity intact. Now we can make use of this destructured state to push new data into it. Now let's take a look at how vModel would work if we're using that with a Pinnia store. We can use it to bind something like our input here to the Pinnia state so that our template stays in sync with the store and reacts accordingly. So those are some common ways to be accessing our Pinnia state. And some key takeaways that I want you to remember is that in an option store, we can use this. In an option store's getter, we pass in state. In a setup store, we access state properties directly. We don't use this. In a component, we can destructure state properties with that storeToRefs helper. And then we can use vModel to bind a value in our component to our store state. All right, so we looked at some ways to be accessing our Pinnia state. Now how do we go about mutating that state? As a refresher, when we look at how vuex worked, when we mutated state, we would dispatch an action. That action would then go ahead and commit a mutation. And this was really the only recommended way to be mutating state unless you maybe were breaking that pattern against those recommended best practices. But of course, Pinnia is different. We no longer have that mutation step, and we have a lot more flexibility now with how we update our state. The most common way to be mutating state within Pinnia is to trigger an action in that store causing that state to be changed. In this example, clicking the add to favorites button triggers the add to favorites action in the favorites store. Now some people are surprised to learn that this is not the only way to change and mutate state within Pinnia. We can also change state directly by assigning a new value to the state property. In this example, we have a watcher on the city value. And then if the user deletes the city, we clear out those state properties directly so the user can start a new search. Another way we can update state is to use the Pinnia patch method. This allows us to apply multiple changes at the same time to a store state. Here, notice how we're passing in an object with the updated state properties inside of it. Now using patch comes with some added benefits because it's going to create a single entry into the dev tools. And if we follow this convention throughout our app, we could easily do a search for patch to locate every place within our application where we're updating state in bulk like this. Another useful thing to note about patch is that it can take an object or a function as its parameter. So for example, if our state property was an array, we could use array methods to be updating our state accordingly. Conveniently, Pinnia also offers a reset method so we can reset a store's entire state to its initial value. In this example, the reset method is used within the store itself to clear out the state of our auth store. Notice how we are in an option store, and so we're using this. We could also use reset in a component like so. Using the reset method like this is super useful when we want to update an entire store at once, like when a user navigates to a certain page. For instance, let's look at this example in the router file where we use Pinnia's reset method in the router itself. So here we're saying whenever the user navigates back to the home page, clear out that previous search. Now unfortunately, the reset method is not available with setup stores, so this is one of the ways where the option store definitely has an advantage over the setup store. And if you're wondering why this is the case, it's because the reset method relies on the state function to create a fresh state, replacing the current store.state with a new one. But we don't have access to the state function in a setup store. But if you still want that functionality where you reset an entire store, but it's a setup store so you can't use a reset method, you could write your own setup function that achieves the same thing, where you write an action that resets the entire store, like so. When it comes to mutating our state, Pinnia also offers a helpful method that we can use to get very detailed info about our actions. This is called the onAction method. And as you can see, this method has a number of hooks we can use to perform some logic when a certain action happens. Notice how we can pass in state as a second argument in case we need it. Here, in this example, it's being used to log information about which action has been triggered in the auth store. To see this working, we can look inside the console and we indeed see the name of the action as well as the arguments that were passed into it. So as you can see, Pinnia is an evolved, flexible way we can architect and manage the state in our applications. If you want to keep learning about the features and patterns within Pinnia, I do invite you to take the Proven Pinnia patterns course over at ViewMastery.com. That's going to include everything you just saw in this talk, plus more, where we explore plugins and how to extend the functionality of Pinnia with your own custom behaviors. And on our website, we have a full playlist of Pinnia courses. So if you or your teammates need to start with the fundamentals, you can build a to-do app with us. We also have a Q&A with the creator of Pinnia, Eduardo, so you can learn a bit about common questions that people have and his answers to them. We just released an exclusive course taught by Eduardo called Five Elegant Ways to Use Pinnia. So you can really get inside the mind of the person who wrote the library and use it like a pro. And of course, being the ultimate learning platform for vue developers means we have content about topics across the board. So if you're looking for Nuxt content, you can build a blog with the Nuxt content module. You can learn about Nuxt middleware, the Nuxt server, start to get into more full stack Nuxt concepts. If you're a fan of the composables from the vue Use Library, you can take our Coding Better Composables course and learn how to create your own from scratch. You can learn how to use utility for css with the popular Tailwind library. And we have exclusive content taught by the creator of vue and vite himself, Evan Yu. All these courses and more can be found in our comprehensive library of content that you can use to become the best vue developer you possibly can be. If you're part of a team that's using vue, I invite you to join together under a team account. And when you do, you can get a 55% discount to access all of our content. Of course, if you're an individual, we have monthly and annual plans to have access to our content. And finally, if you want a free resource that's going to help you learn Pinnia fast, go grab the free Pinnia cheat sheet at the URL on your screen. I very much appreciate your time and attention, and I hope to see you over at vuemastery.com.
20 min
15 May, 2023

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