Since the introduction of the composition API, I have wondered if will it be the end of state stores like Vuex. But Pinia offers a win-win concept to try in your current application.

Why did we need state stores?

Sharing state and its mutations in a “props-down, events up” approach became inconvenient when a contextual state like the authenticated user needed to be passed around everywhere. Also when such a state was drilled deep into the component hierarchy.

When Vuex was introduced, it was the solution for sharing these kinds of state contexts. You define a state store and interested components could read and mutate the state with little overhead.

Here is an example of a current-user store:

js// initial state
const state = () => ({
  currentUser: null,
});

// actions
const actions = {
  async login({ commit }) {
    const response = await fetch('/login', {
      method: 'post',
      body: JSON.stringify({ email, password }),
    }).then((r) => r.json());

    commit('setUser', response.user);
  },
};

// mutations
const mutations = {
  setUser(state, user) {
    state.currentUser = user;
  },
};

export default {
  state,
  getters,
  actions,
  mutations,
};

Vue state landscape after the composition API

The introduction of the composition API allowed the creation of reactive states. Components could share and mutate them in the same way as a state store.

Here is how you could get the same thing as above:

jsimport { ref } from 'vue';

const currentUser = ref(null);

export function useCurrentUser() {
  async function login(email, password) {
    const response = await fetch('/login', {
      method: 'post',
      body: JSON.stringify({ email, password }),
    }).then((r) => r.json());

    currentUser.value = response.user;
  }

  return {
    currentUser,
    login,
  };
}

On top of that, it offered a more complete reactive ecosystem with watchers, effects, and much better TypeScript support.

However, we did not gain this without losing anything. Since we are no longer using a managed state store, it meant you lost a few advantages of state stores like:

Devtools and debugging

You can’t track the state changes and what caused mutations in your states. Also, timelines and loading serialized state for debugging. Hot module replacement

State is more likely to be cleared when you change something during development.

Mutation strictness

If you use plain ref or reactive then your state becomes mutable to all components. Meaning you no longer force components to mutate the state through something like mutations.

However, you could get the same effect by using readonly composition to get your state locked down.

jsimport { readonly, ref } from 'vue';

const currentUser = ref(null);

export function useCurrentUser() {
  async function login(email, password) {
    // ...
  }

  return {
    // only the login function can now mutate the user
    currentUser: readonly(currentUser),
    login,
  };
}

To be honest, I never needed those debugging features in my daily work, the most important thing for me was state sharing. A lot of developers and Vue community authors seem to have similar opinions.

To re-iterate, I think the composition API did not necessarily eliminate the need for state management. More like it “significantly reduced” that need.

However, I would say that if you make sure your composition functions are small and well organized, you probably won’t need a state management solution. But that’s a huge “if”.

You probably will keep adding more complex logic, and slowly you might abandon the restrictions you introduce like with readonly in the previous example. You could reach a point where you no longer can track what or why a state was changed and you might be on the lookout for new store management solutions. So back to square one.

Pinia

Pinia is the new recommended state management solution for Vue 3, it offers a similar API to Vuex but fixes most of its issues. The highlights for me are:

  • Simpler API with less stuff to learn
  • Vastly better TypeScript support
  • Both composition API and options API support

Pinia has a very good read on how it differs from Vuex, so be sure to read it here.

However, the same question arises. If you don’t need the debugging experience of a state management solution and already using the composition API, then why bother, right?

I even heard that if you use the options API, then you should use Pina. It’s either you use the composition API and wouldn’t need it or you use the options API and you would. It’s one or the other, right?

Pinia has an interesting feature up its sleeve that I have stumbled upon recently that voids the first half of that opinion. You don’t have to compromise with the debugging experience if you are using the composition API with Pinia with this neat feature.

Pinia and composition functions

I think this is only mentioned once on the Pinia documentation, but you can actually use a setup function to build your store. This means you could use ref to declare state, and regular functions to define actions.

jsimport { defineStore } from 'pinia';
import { ref } from 'vue';

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0);
  function increment() {
    count.value++;
  }

  return { count, increment };
});

In other words, you could pass your existing composition functions into Pinia’s defineStore and get the best of both worlds. Here is how we would do it with the initial authenticated user example:

jsimport { ref } from 'vue';

const currentUser = ref(null);

function useCurrentUser() {
  // same thing...
}

export const useCurrentUserStore = defineStore(
  'currentUser',
  userCurrentUser,
);

This store would work exactly in the same way in your components, and if you open your Vue dev tools, you get all those nice timelines and state debugging utilities. This isn’t limited to your composition functions, it also extends to any 3rd party composition functions. This opens the door to so many things. Allow me to demonstrate with a few 3rd party libraries.

Pinia + Villus (GraphQL)

A shameless plug here, so villus is a small Vue GraphQL library built by yours truly. It is meant to be a minimal alternative to the apollo ecosystem libraries if you don’t need all of their features. Anyways, since villus mainly offers composition API functions that you use to perform queries and mutations on your GraphQL API, it means you can use villus as a composition source for your Pinia stores.

Here is a quick running example:

The beauty of this is all of the reactive state that villus functions expose will all be tracked by Pinia. And any functions exposed will be treated as actions. It fits like a glove and truly shows the awesomeness of the composition API when embraced like this by a state store library like Pinia. This is one way to solve the long-standing problem of using GraphQL libraries inside your state stores.

Pinia + VeeValidate

I’m an advocate of not putting your form state into state stores because they are hardly shareable, with some exceptions of course. But an interesting use case here is you can do the same thing with composable form libraries like vee-validate.

You can use some of the composition functions offered by vee-validate like useForm as a composition source for your Pinia stores. Here is a not-very-practical-example (still cool) of Pinia taking over vee-validate useForm API and re-exposing it as a state store.

Conclusion

This Pinia “composition-function-as-a-store” takes what we already like and love about the composition API, then spices it up with the DX features that it offers.

Try it out in your application if you already use Pinia.