Building Pinia Stores from Composition API
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.