The Problem
The Pinia docs offer two distinct syntax for writing Pinia stores.
However when asked, “Which syntax should I pick”, the docs simply say to “pick the one that you feel the most comfortable with”.
“You do not yet realize [their] importance. You have only begun to discover your power. Join me and I will complete your training. With our combined strength, we can end this destructive conflict and bring order to the galaxy.”
– Darth Vader
Rather than having to arbitrarily choose a syntax, or have one chosen for you. This post aims to help you “search your feelings” to know which is true or right…for your project.
The Syntactic Difference
Option Store
The Option store is named for its resemblance to Vue’s Options API. With this approach, the second argument to defineStore()
is an Object of options. The included defaults are state, getters, and actions; however, the “options” can be extended via plugins, either custom or from the community.
import { defineStore } from 'pinia';
export const useOptionStore = defineStore('options', {
state: () => {
return {
count: 0,
};
},
getters: {
doubleCount(state) {
return state.count * 2;
},
},
actions: {
incrementCount() {
return this.count++;
},
clearCount() {
return (this.count = 0);
},
},
});
Setup Store
The Setup store gets its name from its resemblance to the setup option/method/function used in Vue’s Composition API. In this approach, the second argument to defineStore()
is a function that returns an object with properties. That is, any property of any name or type. I should note that even if the same names are used as in the Option store (state, getters, actions) the Pinia store behaves differently… more on that later.
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useSetupStore = defineStore('setup', () => {
const count = ref(0);
function incrementCount() {
return count.value++;
}
function clearCount() {
return (count.value = 0);
}
return {
count,
doubleCount: computed(() => count.value * 2),
incrementCount,
clearCount,
};
});
The Practical Difference – Option Store
Structure
If state management libraries are known for anything, besides managing state, its structure. They’ve got mad boilerplates. And boilerplates that make you mad. But the boilerplate for Pinia Option stores is so simple, small, and straightforward that I consider it a benefit. A reliable structure that pervades the documentation and interplays and allows for reliable extension with plugins.
It is also familiar. Pinia, in a sense, is Vuex without “Mutations”. It borrows so heavily from Vuex design that Evan You called it the de facto Vuex 5. So this structure is familiar to those that have used Vuex in the past. Using the Option store also provides a familiar pattern throughout your application. Once the pattern has been implemented, developers can easily recognize how to write and extend Pina stores.
$pecial methods
Because of the structure, when using the Option store, there are special built-in methods that can be used to do…special things. These special methods are signified by their leading “$”
$reset()
– Resets the state of the store to its initial value.
$patch()
– Allows more than one update to state at a time.
$subscribe()
– Watch state and callback after a mutation (a $patch()
is considered to be a single mutation).
$onAction()
– Subscribe to an action with hooks to run before, after, or onError of an action.
And two references to use when making plugins:
$state
– Used to connect directly to Pinia state.
$options
– Used to attach a custom property to Pinia stores.
It is worth noting that this functionality is achievable in both store types; however these built-in methods can only work when using the Option store. In Setup stores, these functionalities must be written and maintained. Which can quickly lead to writing a library on top of Pinia.
Automatically Reactive
Another nicety of the Option store is that everything is already wrapped in Vue 3’s reactivity. Meaning, the developer does not have to think about what is a ref()
, reactive()
, computed()
. Or with the “$” methods, what is using watch()
or when callbacks should be executed. Pinia does all of the reactive wrapping and unwrapping for you. And has functions like storeToRefs()
to help if you need or prefer a certain structure.
This reduced cognitive load while writing stores provides a better developer experience. Especially if migrating a codebase or for developers new to the concepts of Vue 3 reactivity using the composition api.
The Practical Difference – Setup Store
Flexibility
For all the talk of structure and ease above, it comes at the cost of some flexibility. A Setup store is simply a Javascript function that returns an object. Because of that simplicity, anything can be written inside of that function scope, potentially with more accurate naming than the Pinia defaults provide.
As suggested by its name “Setup store”, I like to think of it as a global setup function that can be accessed anywhere in the application. This means there is a scope for things like a watch()
, Vue composables, or other Pinia stores which the remainder of the scope has access to.
Though Setup stores aren’t “automatically reactive”, developers can choose which elements of Vue’s reactivity to use for every piece of the store. Likewise, it promotes a deeper understanding of the reactivity system as a whole.
Control
Alongside the flexibility comes the opportunity for control. Setup stores functions do not automatically return everything from within its scope. For example, to prevent mutating a reactive variable (ref()
/ reactive()
) directly, one could choose to expose a computed reference and expose a method to mutate the reactive variable in question. Depending on the complexity of a project, this kind of control over mutations could be paramount.
export const useSetupStore = defineStore('setup', () => {
const count = ref(0);
return {
count: computed(() => count.value)
};
});
/* Use in component */
const store = useSetupStore()
// ✅ Can get the value
store.count // 0
// ❌ Can no longer update directly (because its a computed value)
store.count = store.count + 1 // Error
Note: If everything isn’t exposed it does make unit testing stores more tricky
Conclusion
One final thought worth noting. There is no hard rule saying that you must choose just one of these two methods. While it may not be best practice to use multiple patterns. If a project were started implementing the Option store and a new requirement appeared that was much better suited for a Setup store, they can both be used.
Hopefully this post gives some clarity. On the surface, choosing between the two store types may seem as big a choice as choosing between Vue’s Options API and the Composition API. And while they’re syntactically similar, it is much more a choice between using the Option store’s well documented structure and Pinia’s full feature set with the “$” methods and plugin system or more minimally using Pinia’s core functionality to manage state however you wish with the Setup store.
Loved the article? Hated it? Didn’t even read it?
We’d love to hear from you.
Thank you so much for this overview!
Flexibility is good. When you need it! This is true specially when working on a team.
While the Composition API proved it’s value on the jungle of the components, it also requires some effort to avoid every developer to step in each other toes. We ended agreeing in a convention of code order:
1. Static
2. Reactive
3. Computed
4. Functions
5. Mounting executions
6. Watchers
To have watchers at the end have proven to be important.
The Stores, however, are much more simpler. It’s hard to find the need for complex stuff. To use the Option Store brings everything you need to the table while making easier to follow the code written by a coworker.
Sadly, there’s a lot of developers advocating to move from Options to Setup Stores.
Hey! I’m glad that you found the article helpful.
That convention is interesting to think through. As an exercise in code reviews, do you prefer for a piece of code to be a lower number on the list (prefer static over reactive, reactive over computed…etc.)?