Stai perdendo la testa cercando di convertire il tuo state manager da Vuex a Pinia?
Ecco una guida step-by-step per affrontare questo task senza difficoltà.
1. Vuex to Pinia
How to migrate an existing app from Vuex to Pinia?
- Denny Biasiolli -
1
2. WHO AM I
Denny Biasiolli
Full Stack Developer
(JavaScript, Python, Go)
Front End Developer UX/ UI
Fingerprint Supervision Ltd
Italy, Savigliano (CN)
@dennybiasiolli
denny.biasiolli@gmail.com
www.dennybiasiolli.com
2
3. WHAT IS PINIA?
Vuex 5 Pinia is the officially recognized
state management library for Vue.
Started as an experiment to redesign what a Store for
Vue could look like with the Composition API
https://pinia.vuejs.org/introduction.html
3
4. WHAT IS PINIA?
Started out as an exploration of what the next iteration
of Vuex could look like, incorporating many ideas from
core team discussions for Vuex 5.
Pinia already implemented most of what they wanted
in Vuex 5 and decided to make it the new
recommendation instead.
https://pinia.vuejs.org/introduction.html#comparison-with-vuex
4
6. (SMALL) COMPARISON WITH VUEX
3.X/4.X
Pinia works with Vue 2 and Vue 3
Simpler API than Vuex
Mutations no longer exist, often perceived as
extremely verbose.
No need to create custom complex wrappers to
support TypeScript
https://pinia.vuejs.org/introduction.html#comparison-with-vuex
6
7. (SMALL) COMPARISON WITH VUEX
3.X/4.X
No more magic strings to inject
Import the functions, call them, enjoy
autocompletion!
No need to dynamically add stores
They are all dynamic by default.
https://pinia.vuejs.org/introduction.html#comparison-with-vuex
7
8. (SMALL) COMPARISON WITH VUEX
3.X/4.X
No more nested structuring of modules.
You can still use a store inside another.
No namespaced modules.
You could say all stores are namespaced.
https://pinia.vuejs.org/introduction.html#comparison-with-vuex
8
9. INSTALL PINIA
Pinia can coexist with Vuex
Vue 3.x and 2.7.x
Vue 2.6.x
npm install -S pinia
npm install -S @vue/composition-api
npm install -S pinia
https://pinia.vuejs.org/getting-started.html#installation
9
10. ROOT STORE (BASIC)
Vue 3.x
Vue 2.x
// src/main.js
import { createPinia } from 'pinia'
app.use(createPinia())
// src/main.js
import { createPinia, PiniaVuePlugin } from 'pinia'
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
// other options (store, render function, etc)...
pinia,
})
10
11. ROOT STORE (ADVANCED)
Vue 3.x
Vue 2.x
// src/stores/index.js
import { createPinia } from 'pinia'
export default createPinia()
// src/stores/index.js
import { createPinia, PiniaVuePlugin } from 'pinia'
import Vue from 'vue'
Vue.use(PiniaVuePlugin)
export default createPinia()
11
12. ROOT STORE (ADVANCED)
Vue 3.x
Vue 2.x
// src/main.js
import pinia from './stores'
app.use(pinia)
// src/main.js
import pinia from './stores'
new Vue({
// other options (store, render function, etc)...
pinia,
})
12
13. DEFINING A STORE
Vue 3.x/2.x
// src/stores/main.js
import { defineStore } from 'pinia'
// the first argument is a unique id of the store
export const useStore = defineStore('main', {
state: () => ({
count: 0,
}),
getters: {
isEven: state => state.count % 2 === 0,
isOdd() {
return !this.isEven
},
},
13
28. TEST MIGRATION
state: no changes
getters
use .call on a getter when using this to access
other getters
-expect(getters.isOdd({}, { isEven: false })).toBe(true)
+expect(getters.isOdd.call({ isEven: false })).toBe(true)
28
29. TEST MIGRATION
mutations: → actions
actions
use .call to pass state/getters/actions
-const context = { commit: vi.fn() }
-actions.incrementAsync(context)
+const state = { increment: vi.fn() }
+actions.incrementAsync.call(state)
// ...
-expect(context.commit).toHaveBeenCalled()
+expect(state.increment).toHaveBeenCalled()
29
30. STATE TEST MIGRATION
import { test, expect } from 'vitest'
import { defaultState } from '@/stores/main'
test('defaultState should be as expected', () => {
expect(defaultState).toEqual({
count: 0,
})
})
30
31. GETTERS TEST MIGRATION
import { test, expect } from 'vitest'
import { getters } from '@/stores/main'
test('isEven should work as expected', () => {
expect(getters.isEven({ count: 0 })).toBe(true)
expect(getters.isEven({ count: 1 })).toBe(false)
})
test('isOdd should work as expected', () => {
- expect(getters.isOdd(undefined, { isEven: true }))
+ expect(getters.isOdd.call({ isEven: true }))
.toBe(false)
- expect(getters.isOdd(undefined, { isEven: false }))
+ expect(getters.isOdd.call({ isEven: false }))
.toBe(true)
})
31
32. MUTATIONS TEST MIGRATION
import { test, expect } from 'vitest'
-import { mutations } from '@/store/index'
+import { actions } from '@/stores/main'
test('increment should work as expected', () => {
const state = { count: 0 }
- mutations.increment(state)
+ actions.increment.call(state)
expect(state.count).toBe(1)
- mutations.increment(state)
+ actions.increment.call(state)
expect(state.count).toBe(2)
})
32
33. ACTIONS TEST MIGRATION
import { test, expect, vi } from 'vitest'
-import { actions } from '@/store/index'
+import { actions } from '@/stores/main'
test('incrementAsync should work as expected', () => {
vi.useFakeTimers()
- const context = { commit: vi.fn() }
- actions.incrementAsync(context)
+ const state = { increment: vi.fn() }
+ actions.incrementAsync.call(state)
- expect(context.commit).not.toHaveBeenCalled()
+ expect(state.increment).not.toHaveBeenCalled();
vi.advanceTimersByTime(1000)
- expect(context.commit).toHaveBeenCalledWith('increment')
+ expect(state.increment).toHaveBeenCalled();
vi.useRealTimers()
})
33
39. createTestingPinia
automatically mocks all actions
unit test store and components separately
allows you to overwrite getter values in tests
(not working with Vue 2 and Jest)
39
40. COMPONENT TESTING (VUE 3 + VITEST)
import { createTestingPinia } from '@pinia/testing'
import { shallowMount } from '@vue/test-utils';
import { useStore } from '@/stores/main'
let pinia, store
beforeEach(() => {
pinia = createTestingPinia({
createSpy: vi.fn,
initialState: { main: { count: 0 } },
})
// init store only after creating a testing pinia
store = useStore()
})
// tests
const wrapper = shallowMount(Component, {
global: { plugins: [pinia] }
})
40
41. COMPONENT TESTING (VUE 2 + JEST)
import { PiniaVuePlugin } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { shallowMount, createLocalVue } from '@vue/test-utils'
import { useStore } from '@/stores/main'
const localVue = createLocalVue()
localVue.use(PiniaVuePlugin)
let pinia, store
beforeEach(() => {
pinia = createTestingPinia({
initialState: { main: { count: 0 } },
})
// init store only after creating a testing pinia
store = useStore()
})
// tests
const wrapper = shallowMount(Component, { localVue, pinia })
41
42. COMPONENT TESTING (VUE 2 + JEST)
Getters are not writable, you need to set the correct
state in order to make them work as expected.
store.count = 1
// or
store.$patch({
count: 1,
// other properties
})
42
43. COMPONENT TEST MIGRATION (VUE 3 + VITEST)
+import { createTestingPinia } from '@pinia/testing'
import { describe, test, beforeEach, expect, vi } from 'vites
-import { createStore } from 'vuex'
import { shallowMount } from '@vue/test-utils';
import Counter from '@/components/Counter.vue'
+import { useStore } from '@/stores/main'
43
50. MIGRATION PROBLEMS NOTES
Direct $store usage
Use the correct store
$store.state.propertyName
$store.state.moduleName.propertyName
$store.getters.getterName
$store.getters['moduleName/getterName']
const myStore = useMyStore()
myStore.propertyName
myStore.getterName
50
51. MIGRATION PROBLEMS NOTES
Direct $store commit/dispatch
Use store actions
$store.commit.mutationName()
$store.commit.moduleName.mutationName()
$store.dispatch.actionName()
$store.dispatch['moduleName/actionName']()
const myStore = useMyStore()
myStore.actionName()
51
52. MIGRATION PROBLEMS NOTES
What about a global useStore()?
If used outside of <script setup>,
will give you this error
import { useStore } from '@/stores/main'
const store = useStore()
// other stuff
getActivePinia was called with no active Pinia.
Did you forget to install pinia?
52
53. MIGRATION PROBLEMS NOTES
Can Vuex and Pinia coexist?
Yes, but...
make sure to migrate entire modules,
not entire components.
53
54. MIGRATION PROBLEMS NOTES
How to test a real store?
import { setActivePinia, createPinia } from 'pinia'
beforeEach(() => {
setActivePinia(createPinia())
})
test('should work as expected', () => {
const store = useStore()
// ...
})
54
55. MIGRATION PROBLEMS NOTES
What about store persistence?
myStore.$subscribe((mutation, state) => {
localStorage.setItem('myStore', JSON.stringify(state))
})
// restore with
myStore.$state = { /* ... */ }
watch(
pinia.state,
(state) => {
localStorage.setItem('piniaState', JSON.stringify(state))
},
{ deep: true }
)
// restore with
pinia.state.value = { /* ... */ }
https://pinia.vuejs.org/core-concepts/state.html#subscribing-to-the-state
55
56. FINAL TASKS
1. remove Vuex store from main.js
-import store from './store'
// Vue 3
-app.use(store)
// Vue 2
new Vue({
router,
- store,
pinia,
render: h => h(App),
}).$mount('#app')
56
57. FINAL TASKS
2. delete Vuex store and tests
3. uninstall Vuex dependencies
rm -rf src/store
rm -rf tests/unit/store
npm uninstall vuex @vue/cli-plugin-vuex
57