Skip to content

Commit 7f6faac

Browse files
BenSheltonposva
andauthored
docs: Vuex Migration Docs (#795)
Co-authored-by: Eduardo San Martin Morote <[email protected]>
1 parent fc386d8 commit 7f6faac

File tree

5 files changed

+295
-1
lines changed

5 files changed

+295
-1
lines changed

packages/docs/.vitepress/config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ module.exports = {
216216
text: 'Cookbook',
217217
link: '/cookbook/',
218218
children: [
219+
{
220+
text: 'Migration from Vuex ≤4',
221+
link: '/cookbook/migration-vuex.html',
222+
},
219223
{
220224
text: 'Hot Module Replacement',
221225
link: '/cookbook/hot-module-replacement.html',

packages/docs/cookbook/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Cookbook
22

3+
- [Migrating from Vuex ≤4](./migration-vuex.md): A migration guide for converting Vuex ≤4 projects.
34
- [HMR](./hot-module-replacement.md): How to activate hot module replacement and improve the developer experience.
45
- [Testing Stores (WIP)](./testing.md): How to unit test Stores and mock them in component unit tests.
56
- [Composing Stores](./composing-stores.md): How to cross use multiple stores. e.g. using the user store in the cart store.

packages/docs/cookbook/migration-v1-v2.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ If you were writing plugins, using TypeScript, and extending the type `DefineSto
6161
}
6262
```
6363

64-
## `PiniaStorePLugin` was renamed
64+
## `PiniaStorePlugin` was renamed
6565

6666
The type `PiniaStorePlugin` was renamed to `PiniaPlugin`.
6767

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
# Migrating from Vuex ≤4
2+
3+
Although the structure of Vuex and Pinia stores is different, a lot of the logic can be reused. This guide serves to help you through the process and point out some common gotchas that can appear.
4+
5+
## Preparation
6+
7+
First, follow the [Getting Started guide](../getting-started.md) to install Pinia.
8+
9+
## Restructuring Modules to Stores
10+
11+
Vuex has the concept of a single store with multiple _modules_. These modules can optionally be namespaced and even nested within each other.
12+
13+
The easiest way to transition that concept to be used with Pinia is that each module you used previously is now a _store_. Each store requires an `id` which is similar to a namespace in Vuex. This means that each store is namespaced by design. Nested modules can also each become their own store. Stores that depend on each other will simply import the other store.
14+
15+
How you choose to restructure your Vuex modules into Pinia stores is entirely up to you, but here is one suggestion:
16+
17+
```bash
18+
# Vuex example (assuming namespaced modules)
19+
src
20+
└── store
21+
├── index.js # Initializes Vuex, imports modules
22+
└── modules
23+
├── module1.js # 'module1' namespace
24+
└── nested
25+
├── index.js # 'nested' namespace, imports module2 & module3
26+
├── module2.js # 'nested/module2' namespace
27+
└── module3.js # 'nested/module3' namespace
28+
29+
# Pinia equivalent, note ids match previous namespaces
30+
src
31+
└── stores
32+
├── index.js # (Optional) Initializes Pinia, does not import stores
33+
├── module1.js # 'module1' id
34+
├── nested-module2.js # 'nested/module3' id
35+
├── nested-module3.js # 'nested/module2' id
36+
└── nested.js # 'nested' id
37+
```
38+
39+
This creates a flat structure for stores but also preserves the previous namespacing with equivalent `id`s. If you had some state/getters/actions/mutations in the root of the store (in the `store/index.js` file of Vuex) you may wish to create another store called something like `root` which holds all that information.
40+
41+
The directory for Pinia is generally called `stores` instead of `store`. This is to emphasize that Pinia uses multiple stores, instead of a single store in Vuex.
42+
43+
For large projects you may wish to do this conversion module by module rather than converting everything at once. You can actually mix Pinia and Vuex together during the migration so this approach can also work and is another reason for naming the Pinia directory `stores` instead.
44+
45+
## Converting a Single Module
46+
47+
Here is a complete example of the before and after of converting a Vuex module to a Pinia store, see below for a step-by-step guide. The Pinia example uses an option store as the structure is most similar to Vuex:
48+
49+
```ts
50+
// Vuex module in the 'auth/user' namespace
51+
import { Module } from 'vuex'
52+
import { api } from '@/api'
53+
import { RootState } from '@/types' // if using a Vuex type definition
54+
55+
interface State {
56+
firstName: string
57+
lastName: string
58+
userId: number | null
59+
}
60+
61+
const storeModule: Module<State, RootState> = {
62+
namespaced: true,
63+
state: {
64+
firstName: '',
65+
lastName: '',
66+
userId: null
67+
},
68+
getters: {
69+
firstName: (state) => state.firstName,
70+
fullName: (state) => `${state.firstName} ${state.lastName}`,
71+
loggedIn: (state) => state.userId !== null,
72+
// combine with some state from other modules
73+
fullUserDetails: (state, getters, rootState, rootGetters) => {
74+
return {
75+
...state,
76+
fullName: getters.fullName,
77+
// read the state from another module named `auth`
78+
...rootState.auth.preferences,
79+
// read a getter from a namespaced module called `email` nested under `auth`
80+
...rootGetters['auth/email'].details
81+
}
82+
}
83+
},
84+
actions: {
85+
async loadUser ({ state, commit }, id: number) {
86+
if (state.userId !== null) throw new Error('Already logged in')
87+
const res = await api.user.load(id)
88+
commit('updateUser', res)
89+
}
90+
},
91+
mutations: {
92+
updateUser (state, payload) {
93+
state.firstName = payload.firstName
94+
state.lastName = payload.lastName
95+
state.userId = payload.userId
96+
},
97+
clearUser (state) {
98+
state.firstName = ''
99+
state.lastName = ''
100+
state.userId = null
101+
}
102+
}
103+
}
104+
105+
export default storeModule
106+
```
107+
108+
```ts
109+
// Pinia Store
110+
import { defineStore } from 'pinia'
111+
import { useAuthPreferencesStore } from './auth-preferences'
112+
import { useAuthEmailStore } from './auth-email'
113+
import vuexStore from '@/store' // for gradual conversion, see fullUserDetails
114+
115+
interface State {
116+
firstName: string
117+
lastName: string
118+
userId: number | null
119+
}
120+
121+
export const useAuthUserStore = defineStore('auth/user', {
122+
// convert to a function
123+
state: (): State => ({
124+
firstName: '',
125+
lastName: '',
126+
userId: null
127+
}),
128+
getters: {
129+
// firstName getter removed, no longer needed
130+
fullName: (state) => `${state.firstName} ${state.lastName}`,
131+
loggedIn: (state) => state.userId !== null,
132+
// must define return type because of using `this`
133+
fullUserDetails (state): FullUserDetails {
134+
// import from other stores
135+
const authPreferencesStore = useAuthPreferencesStore()
136+
const authEmailStore = useAuthEmailStore()
137+
return {
138+
...state,
139+
// other getters now on `this`
140+
fullName: this.fullName,
141+
...authPreferencesStore.$state,
142+
...authEmailStore.details
143+
}
144+
145+
// alternative if other modules are still in Vuex
146+
// return {
147+
// ...state,
148+
// fullName: this.fullName,
149+
// ...vuexStore.state.auth.preferences,
150+
// ...vuexStore.getters['auth/email'].details
151+
// }
152+
}
153+
},
154+
actions: {
155+
// no context as first argument, use `this` instead
156+
async loadUser (id: number) {
157+
if (this.userId !== null) throw new Error('Already logged in')
158+
const res = await api.user.load(id)
159+
this.updateUser(res)
160+
},
161+
// mutations can now become actions, instead of `state` as first argument use `this`
162+
updateUser (payload) {
163+
this.firstName = payload.firstName
164+
this.lastName = payload.lastName
165+
this.userId = payload.userId
166+
},
167+
// easily reset state using `$reset`
168+
clearUser () {
169+
this.$reset()
170+
}
171+
}
172+
})
173+
```
174+
175+
Let's break the above down into steps:
176+
177+
1. Add a required `id` for the store, you may wish to keep this the same as the namespace before
178+
2. Convert `state` to a function if it was not one already
179+
3. Convert `getters`
180+
1. Remove any getters that return state under the same name (eg. `firstName: (state) => state.firstName`), these are not necessary as you can access any state directly from the store instance
181+
2. If you need to access other getters, they are on `this` instead of using the second argument. Remember that if you are using `this` then you will have to use a regular function instead of an arrow function. Also note that you will need to specify a return type because of TS limitations, see [here](../core-concepts/getters.md#accessing-other-getters) for more details
182+
3. If using `rootState` or `rootGetters` arguments, replace them by importing the other store directly, or if they still exist in Vuex then access them directly from Vuex
183+
4. Convert `actions`
184+
1. Remove the first `context` argument from each action. Everything should be accessible from `this` instead
185+
2. If using other stores either import them directly or access them on Vuex, the same as for getters
186+
5. Convert `mutations`
187+
1. Mutations do not exist any more. These can be converted to `actions` instead, or you can just assign directly to the store within your components (eg. `userStore.firstName = 'First'`)
188+
2. If converting to actions, remove the first `state` argument and replace any assignments with `this` instead
189+
3. A common mutation is to reset the state back to its initial state. This is built in functionality with the store's `$reset` method. Note that this functionality only exists for option stores.
190+
191+
As you can see most of your code can be reused. Type safety should also help you identify what needs to be changed if anything is missed.
192+
193+
## Usage Inside Components
194+
195+
Now that your Vuex module has been converted to a Pinia store, any component or other file that uses that module needs to be updated too.
196+
197+
If you were using `map` helpers from Vuex before, it's worth looking at the [Usage without setup() guide](./options-api.md) as most of those helpers can be reused.
198+
199+
If you were using `useStore` then instead import the new store directly and access the state on it. For example:
200+
201+
```ts
202+
// Vuex
203+
import { defineComponent, computed } from 'vue'
204+
import { useStore } from 'vuex'
205+
206+
export default defineComponent({
207+
setup () {
208+
const store = useStore()
209+
210+
const firstName = computed(() => store.state.auth.user.firstName)
211+
const fullName = computed(() => store.getters['auth/user/firstName'])
212+
213+
return {
214+
firstName,
215+
fullName
216+
}
217+
}
218+
})
219+
```
220+
221+
```ts
222+
// Pinia
223+
import { defineComponent, computed } from 'vue'
224+
import { useAuthUserStore } from '@/stores/auth-user'
225+
226+
export default defineComponent({
227+
setup () {
228+
const authUserStore = useAuthUserStore()
229+
230+
const firstName = computed(() => authUserStore.firstName)
231+
const fullName = computed(() => authUserStore.fullName)
232+
233+
return {
234+
// you can also access the whole store in your component by returning it
235+
authUserStore,
236+
firstName,
237+
fullName
238+
}
239+
}
240+
})
241+
```
242+
243+
## Usage Outside Components
244+
245+
Updating usage outside of components should be simple as long as you're careful to _not use a store outside of functions_. Here is an example of using the store in a Vue Router navigation guard:
246+
247+
```ts
248+
// Vuex
249+
import vuexStore from '@/store'
250+
251+
router.beforeEach((to, from, next) => {
252+
if (vuexStore.getters['auth/user/loggedIn']) next()
253+
else next('/login')
254+
})
255+
```
256+
257+
```ts
258+
// Pinia
259+
import { useAuthUserStore } from '@/stores/auth-user'
260+
261+
router.beforeEach((to, from, next) => {
262+
// Must be used within the function!
263+
const authUserStore = useAuthUserStore()
264+
if (authUserStore.loggedIn) next()
265+
else next('/login')
266+
})
267+
```
268+
269+
More details can be found [here](../core-concepts/outside-component-usage.md).
270+
271+
## Advanced Vuex Usage
272+
273+
In the case your Vuex store using some of the more advanced features it offers, here is some guidance on how to accomplish the same in Pinia. Some of these points are already covered in [this comparison summary](../introduction.md#comparison-with-vuex-3-x-4-x).
274+
275+
### Dynamic Modules
276+
277+
There is no need to dynamically register modules in Pinia. Stores are dynamic by design and are only registered when they are needed. If a store is never used, it will never be "registered".
278+
279+
### Hot Module Replacement
280+
281+
HMR is also supported but will need to be replaced, see the [HMR Guide](./hot-module-replacement.md).
282+
283+
### Plugins
284+
285+
If you use a public Vuex plugin then check if there is a Pinia alternative. If not you will need to write your own or evaluate whether the plugin is still necessary.
286+
287+
If you have written a plugin of your own, then it can likely be updated to work with Pinia. See the [Plugin Guide](../core-concepts/plugins.md).

packages/docs/introduction.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,5 @@ Pinia API is very different from Vuex ≤4, namely:
179179
- No need to dynamically add stores, they are all dynamic by default and you won't even notice. Note you can still manually use a store to register it whenever you want but because it is automatic you don't need to worry about it.
180180
- No more nested structuring of _modules_. You can still nest stores implicitly by importing and _using_ a store inside another but Pinia offers a flat structuring by design while still enabling ways of cross composition among stores. **You can even have circular dependencies of stores**.
181181
- No _namespaced modules_. Given the flat architecture of stores, "namespacing" stores is inherent to how they are defined and you could say all stores are namespaced.
182+
183+
For more detailed instructions on how to convert an existing Vuex ≤4 project to use Pinia, see the [Migration from Vuex Guide](./cookbook/migration-vuex.md).

0 commit comments

Comments
 (0)