Choosing Between Two Store Syntaxes in Pinia

When Two Pinia Collide. Do we need one for each hand?

Choosing Between Two Store Syntaxes in Pinia

The Problem



The Pinia docs offer two distinct syntax for writing Pinia stores.

  1.  Option Store
  2.  Setup Store

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

 

Pinia logo acting out the Darth Vader I am your father scene.

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

Star Wars which syntax will you choose?

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.

Reach Out

Leave a comment

Leave a Reply

Your email address will not be published. Required fields are marked *

More Insights

View All