skip to content

Moving a Vue 3 app's namespaced Vuex modules (auth, cart, catalog) to Pinia one store at a time, how do you convert cart while auth still lives in Vuex?

level: seniorimportance: should knowfreq 34%

answer

  1. both plugins installed at once
  2. id first, state as a function
  3. context argument becomes this
  4. same-name getters must go
  5. read Vuex until auth moves

basics

~20 s

Install Pinia beside Vuex, rewrite cart as defineStore('cart', …) with a state function, mutations folded into actions and context replaced by this, read auth from the Vuex store instance until it moves, then switch cart's callers and delete the Vuex module.

solid answer

~50 s

Pinia and Vuex can run in the same app, so you install both and convert one module at a time. For `cart`, following the migration guide: give it the id `'cart'`; make `state` a function; delete getters that only return a state property of the same name; rewrite getters that used `rootGetters` to read the Vuex store instance for `auth` (or import the Pinia store once it exists); drop each action's `context` argument and use `this`; turn mutations into actions or plain assignments, and use `$reset()` for a full reset, which works because this is an option store. Then change every caller of `cart` — `useStore()` string paths, map helpers — to `useCartStore()`, and remove `cart` from the Vuex store in the same change, so there is never a second source of truth. Remaining Vuex modules that need the cart call `useCartStore()` inside their functions.

code

ts · 7 lines
ts
// main.ts — both stores installed while modules move one by one
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import vuexStore from './store'
import App from './App.vue'

createApp(App).use(vuexStore).use(createPinia()).mount('#app')

go deeper

for a junior

Recall the conversion steps: an id, state as a function, no mutations, and actions that use this instead of a context argument.

for a middle

Explain each getter rewrite: same-name getters are deleted, this reads other getters, and rootGetters becomes a read of another store.

for a senior

Show how you would sequence modules by dependency, bridge Pinia and Vuex temporarily, and switch callers in the same change so state never has two owners.

for a principal

Frame the order as a risk decision: start where bridges are few and value is visible, and stop migrating modules nobody is changing.

## The plan: two stores in one app The migration guide explicitly allows **mixing Pinia and Vuex** during a migration, which is what makes a store-by-store move possible. The app installs both plugins; converted modules live in `stores/`, the rest stay in the Vuex `store/` folder, and each module is moved in its own change. For a legacy shop whose namespaced modules are `auth`, `cart` and `catalog`, converting `cart` first while `auth` stays in Vuex is a typical starting point: `cart` depends on `auth` (checkout needs a signed-in user), but little depends on `cart`. ## Converting the module, step by step The guide's recipe, applied to `cart`: 1. **Give the store an id.** `defineStore('cart', { … })` — keeping the old namespace as the id, in camelCase. The id is the first argument; the `{ id: … }` object form was removed in Pinia 3. 2. **Make `state` a function** if the module used a plain object: `state: () => ({ items: [], couponCode: '' })`. 3. **Convert getters.** - Delete getters that only return a state property under the same name, such as `items: (state) => state.items`. State is readable on the store directly, and Pinia flags a getter that shares a name with a state property in development. - Getters that used the `getters` argument read other getters through `this`, as regular functions — with an explicit return type in TypeScript. - Getters that used `rootState` or `rootGetters` read the other store directly: the Vuex store instance while that module is still in Vuex, or its Pinia store once it moves. 4. **Convert actions.** Remove the `context` first argument; everything — state, getters, other actions — is on `this`. 5. **Convert mutations.** Each becomes an action (drop the `state` argument, write through `this`) or disappears in favour of direct assignment. A mutation that reset the module becomes `$reset()`, which exists only on **option stores** — one reason the guide uses an option store for conversions. ## Bridging to modules still in Vuex - **Pinia reading Vuex:** import the Vuex store instance and read `vuexStore.getters['auth/loggedIn']` inside the getter or action. It is a temporary bridge with a string path; it gets replaced when `auth` moves. - **Vuex reading Pinia:** a remaining Vuex module that used `rootState.cart` calls `useCartStore()` inside its getter or action body and reads the property. - **Never at module top level:** both bridges run inside functions, after both plugins are installed on the app. - **One owner per piece of state:** the cart's data lives only in the Pinia store after the switch. Leaving the old Vuex module registered "as a fallback" guarantees two diverging carts. ## Switching the callers Every consumer of `cart` changes in the same change as the store: - composition components replace `useStore()` from `vuex` plus `store.getters['cart/total']` with `const cart = useCartStore()` and `cart.total`; - Options API components replace Vuex's map helpers with Pinia's `mapState`, `mapWritableState`, `mapActions` or `mapStores`; - tests that built a Vuex store for `cart` build a pinia instead. TypeScript helps here: once the Vuex module is gone, any remaining string path or missing property shows up as an error. ## Ordering the remaining modules | Module | Depends on | Depended on by | Move when | |---|---|---|---| | `cart` | `auth` | few | first: its bridge is one getter read | | `catalog` | `auth` preferences | `cart` for prices | second, or together with a cart feature change | | `auth` | nothing | everything | last, or early if bridges multiply | The rule of thumb: move modules with **few inbound dependents** first, because each Pinia store that still reads Vuex needs only an outbound bridge, while a module many others read needs many. Each finished step should leave the app shippable: both plugins installed, every module owned by exactly one library, and the bridges listed somewhere so the last conversion can remove them — along with the Vuex dependency itself. ## Traps - Converting to a **setup store** and then calling `$reset()` throws in development; write a reset action instead. - Copying a `defineStore({ id: 'cart', … })` snippet from old material fails on Pinia 3 and 4. - Keeping a getter with the same name as a state property triggers a development diagnostic. - Forgetting that a Pinia action can call another action directly with `this.add(item)` leads to reintroducing commit-style indirection.

  • What changes in the cart store once auth moves to Pinia as well?
    The `vuexStore.getters['auth/loggedIn']` bridge is replaced by `useAuthStore().loggedIn`, called inside the getter. The import of the Vuex store disappears from the cart file, and the string path is gone, so a renamed auth getter now fails type-checking instead of silently returning undefined.
  • Why does the migration guide convert modules to option stores rather than setup stores?
    An option store has the same state, getters and actions sections as a Vuex module, so the conversion is mechanical, and it has a built-in `$reset()` for reset mutations. A setup store is fine later, but it needs its own reset action and a rewrite of every getter into `computed`.
  • How do you keep the store-by-store move safe when a remaining Vuex module still reads the cart?
    Change that module in the same change as the cart conversion: it calls `useCartStore()` inside its getter or action and reads the property. Then remove the cart module from the Vuex store, so no code can read a stale Vuex copy.

saying these in an interview costs you the question

  • Pinia and Vuex cannot run together, so every module must move in one release.
  • Keep Vuex getters that only return state; Pinia needs them to expose it.
  • Pinia actions still receive a context object as their first argument.
  • $reset() works the same in setup stores, so the store style does not matter.
  • Leave the old Vuex cart registered as a fallback while callers move over.