Replace Vuex with Pinia
INFO
This document represents an architecture decision record (ADR) and has been mirrored from the ADR section in our Shopware 6 repository. You can find the original version here
ADR: Replace Vuex with Pinia
Context
It was brought to our attention that the latest version of Vuex 4.1.0
contains a bug that destroys getter reactivity under specific circumstances. The proposed fix was to downgrade to 4.0.2
. However, downgrading was not possible as 4.0.2
contains other bugs that caused modules to fail.
Decision
Pinia is the new documented standard with Vue 3; therefore, we will switch to Pinia.
Consequences
Removal of Vuex
Below you will find an overview of what will be removed on which Shopware Version.
6.7
For Shopware 6.7 we want to transition all our modules but still leave the possibility for you to use Vuex for your own states.
- All
Shopware.State
functions will cause warnings to appear in the DevTools. For exampleShopware.State.registerModule is deprecated. Use Shopware.Store.register instead!
- All Vuex state definitions will be transitioned to Pinia:
- src/module/sw-bulk-edit/state/sw-bulk-edit.state.js
- src/module/sw-product/page/sw-product-detail/state.js
- src/module/sw-category/page/sw-category-detail/state.js
- src/module/sw-extension/store/extensions.store.ts
- src/module/sw-settings-payment/state/overview-cards.store.ts
- src/module/sw-settings-seo/component/sw-seo-url/state.js
- src/module/sw-settings-shipping/page/sw-settings-shipping-detail/state.js
- src/app/state/notification.store.js
- src/app/state/session.store.js
- src/app/state/system.store.js
- src/app/state/admin-menu.store.js
- src/app/state/admin-help-center.store.ts
- src/app/state/license-violation.store.js
- src/app/state/context.store.ts
- src/app/state/error.store.js
- src/app/state/settings-item.store.js
- src/app/state/shopware-apps.store.ts
- src/app/state/extension-entry-routes.js
- src/app/state/marketing.store.js
- src/app/state/extension-component-sections.store.ts
- src/app/state/extensions.store.ts
- src/app/state/tabs.store.ts
- src/app/state/menu-item.store.ts
- src/app/state/extension-sdk-module.store.ts
- src/app/state/modals.store.ts
- src/app/state/main-module.store.ts
- src/app/state/action-button.store.ts
- src/app/state/rule-conditions-config.store.js
- src/app/state/sdk-location.store.ts
- src/app/state/usage-data.store.ts
- src/module/sw-flow/state/flow.state.js
- src/module/sw-order/state/order.store.ts
- src/module/sw-order/state/order-detail.store.js
- src/module/sw-profile/state/sw-profile.state.js
- src/module/sw-promotion-v2/page/sw-promotion-v2-detail/state.js
6.8
With Shopware 6.8 we will entirely remove everything Vuex related including the dependency.
Shopware.State
- Will be removed. UseShopware.Store
instead.src/app/init-pre/state.init.ts
- Will be removed. Usesrc/app/init-pre/store.init.ts
instead.src/core/factory/state.factory.ts
- Will be removed without replacement.- Interface
VuexRootState
will be removed fromglobal.types.ty
. UsePiniaRootState
instead. - Package
vuex
will be removed.
Transition to Pinia
Pinia calls its state-holding entities stores
. Therefore, we decided to hold everything Pinia-related under Shopware.Store
. The Shopware.Store
implementation follows the Singleton pattern. The private constructor controls the creation of the Pinia root state. This root state must be injected into Vue before the first store can be registered. The init-pre/store.init.ts
takes care of this.
Best practices
- All Pinia Stores must be written in TypeScript
- All Stores will export a type or interface like the
cms-page.state.ts
- The state property of the exported type must be reused for the state definition.
You can always orientate on the cms-page.state.ts
. It contains all best practices.
For now, we have decided to limit the public API of Shopware.Store
to the following:
/**
* Returns a list of all registered Pinia store IDs.
*/
public list(): string[];
/**
* Gets the Pinia store with the given ID.
*/
public get(id: keyof PiniaRootState): PiniaStore;
/**
* Registers a new Pinia store. Works similarly to Vuex's registerModule.
*/
public register(options: DefineStoreOptions): void;
/**
* Unregisters a Pinia store. Works similarly to Vuex's unregisterModule.
*/
public unregister(id: keyof PiniaRootState): void;
The rest of the previous Vuex (Shopware.State
) public API is implemented into Pinia itself.
// Setup
const piniaStore = Shopware.Store.get('...');
// From Vuex subscribe
Shopware.State.subscribe(...);
// To Pinia $subscribe
store.$subscribe(...);
// From Vuex commit
Shopware.State.commit(...);
// To Pinia action call
store.someAction(...);
// From Vuex dispatch
Shopware.State.dispatch(...);
// To Pinia action call
store.someAsyncAction(...);
Example Implementation
To prove that Vuex and Pinia can co-exist during the transition period, we picked a private Vuex state and decided to transition it. We chose the cmsPageState
, which is heavily used in many components. The transition went smoothly without any major disturbances.
How to transition a Vuex module into a Pinia store:
- In Pinia, there are no
mutations
. Place every mutation underactions
. state
needs to be an arrow function returning an object:state: () => ({})
.actions
no longer need to use thestate
argument. They can access everything with correct type support viathis
.- Point 3 also applies to
getters
. - Use
Shopware.Store.register
instead ofShopware.State.registerModule
.
Let's look at a simple Vuex module and how to transition it:
// Old Vuex implementation
Shopware.State.registerModule('example', {
state: {
id: '',
},
getters: {
idStart(state) {
return state.id.substring(0, 4);
}
},
mutations: {
setId(state, id) {
state.id = id;
}
},
actions: {
async asyncFoo({ commit }, id) {
// Do some async stuff
return Promise.resolve(() => {
commit('setId', id);
return id;
});
}
}
});
// New Pinia implementation
// Notice that the mutation setId was removed! You can directly modify a Pinia store state after retrieving it with Shopware.Store.get.
Shopware.Store.register({
id: 'example',
state: () => ({
id: '',
}),
getters: {
idStart: () => this.id.substring(0, 4),
},
actions: {
async asyncFoo(id) {
// Do some async stuff
return Promise.resolve(() => {
this.id = id;
return id;
});
}
}
});