Pinia is a store library for Vue that recently became very popular. It comes with a lot of advantages, it is lightweight, provides strong typescript support, etc. and with Vuex support being dropped, Pinia is basically the new standard. But I won’t go into comparison details. Plenty of information on that could be found, for example in the official documentation and here. What I want to focus on is how to actually switch large scale apps from one to the other.
Those who tried to migrate from Vuex to Pinia over the last year or so have most certainly faced the same issue - file splitting. Although Pinia seems great, it does not provide the same level of abstraction. There are nowadays some workarounds floating in the internet and maybe this blog post comes a little too late to the party. Nevertheless I decided to put it out there for those still looking for a solution and show how our team managed to overcome some struggles.
We started off with an enterprise level project with a classical Vue 3 setup, Vuex for state management, GraphQL endpoint, hooked up to Contentful and multiple external services for the product catalog, blog posts and so on. The most peculiar part was merging content from different sources in order to build particular page types.
You can imagine we had a huge Vuex store, split into multiple modules that were not lazy loaded, and of course there was no code splitting going on. Under these circumstances, going with Pinia seemed like the obvious thing to do.
What was important to us was to have low complexity and keep code readability.
To illustrate what I mean, here is an example of the structure:
You can see we had separate files for each action and a long list of exported functions in each getters.ts and mutations.ts file. And why did we have this separation? Let me use now just a very small real example:
This is a small piece of the Vuex store setup with two actions - the fetchPageIdAction and the fetchPageAction. We would make a request based on the page slug in order to find the page id in a global context. With that page id we are able to retrieve the content we need from the global and local context, merge those and provide to the frontend all the necessary information to render the page. Now imagine a dozen pages like this. I think you understand why file splitting was very much desirable.
Unfortunately when we started with the migration to Pinia, there was not much said on the topic of file splitting. All the examples that could be found were of simple one object stores like:
Furthermore, splitting parts of a Pinia store is not recommended. It seems to break usage of the this object and makes it impossible to access the state in each separate file.
But having in mind the examples above, it didn’t seem like a great idea to put all of that code in one single file. So we were in a ‘pickle’.
As always, the solution was right under our noses. After some trial and error, what we ended up with was importing the store (or what we kept calling a module) in each file - each action file/getters.ts/mutations.ts and declaring it in each function. What used to be the mutations.ts in Vuex was kept as a separate file and imported as part of the actions in the index.ts file.
Simple enough. It worked similarly to Vuex and made it possible to access and modify the state values, keep the reactivity and almost the same structure we previously had.
The last point was exchanging all Vuex store references with the new Pinia stores inside of the components.
This is probably not the most elegant solution, but it worked stably enough in our case and made it possible to keep relatively the same level of abstraction, which is easily readable and traceable, especially in case of onboarding new developers. We were able to take advantage of what Pinia offers and boost the overall performance.
I hope this is a useful example of how file splitting is possible with Pinia and would be helpful in other people’s practice.