skip to content

When moving from Vuex to Pinia, how do nested namespaced modules become flat stores, and what replaces rootState and rootGetters?

level: middleimportance: should knowfreq 41%

answer

  1. no module tree any more
  2. id keeps the old namespace
  3. camelCase ids for mapStores
  4. import the other store instead
  5. created on first use

basics

~20 s

Each Vuex module, nested or not, becomes its own Pinia store whose id keeps the old namespace, such as 'authUser' for 'auth/user'. rootState and rootGetters give way to importing the other store and calling its useStore function inside a getter or action.

solid answer

~40 s

Pinia has no module tree, so the migration guide maps every Vuex module to a separate store in a `stores/` directory and keeps the namespace as the `id`: `'cart'` stays `'cart'`, and a nested `'catalog/filters'` becomes something like `'catalogFilters'` — camelCase, so `mapStores()` produces clean property names. State or getters that sat at the Vuex root can move into a store named, say, `root`. Cross-module reads through `rootState` and `rootGetters` become imports: a getter or action calls `useAuthStore()` and reads its properties, typed and autocompleted. Nothing is registered up front; a store is created the first time its `useStore()` runs, so runtime module registration has no counterpart. Ids must be unique across the whole app, because Pinia caches stores by id alone.

code

ts · 18 lines
ts
// stores/catalog-filters.ts — was the 'catalog/filters' Vuex module
import { defineStore } from 'pinia'
import { useAuthPreferencesStore } from './auth-preferences'

export const useCatalogFiltersStore = defineStore('catalogFilters', {
  state: () => ({ category: '', inStockOnly: false }),
  getters: {
    // was: (state, getters, rootState) => rootState.auth.preferences.currency
    currency: () => useAuthPreferencesStore().currency,
  },
  actions: {
    reset() {
      const prefs = useAuthPreferencesStore()
      this.category = prefs.defaultCategory
      this.inStockOnly = false
    },
  },
})

go deeper

for a junior

Recall that each Vuex module becomes its own Pinia store, and that the store id takes the place of the module namespace.

for a middle

Explain how rootState and rootGetters reads turn into imported stores called inside getters and actions, and why ids should be camelCase and unique.

for a senior

Plan the flattened layout for a real module tree: which nested modules deserve their own store, what goes into a small root store, and how ids map to old namespaces.

for a principal

Treat the flat layout as an architecture decision: fewer implicit paths make dependencies visible, so use the migration to cut coupling rather than recreate the tree.

## From one tree to many stores A **Vuex** application has one store. Its state is a tree assembled from **modules**, each of which may set `namespaced: true` and may contain further modules, so a nested module's state is reached as `store.state.auth.user.firstName` and its getters as `store.getters['auth/user/fullName']`. **Pinia** has no tree. Each store is defined on its own with `defineStore(id, …)`, is imported where it is needed, and is created the first time its `useStore()` function runs. The pinia instance keeps every store's state under `pinia.state.value`, keyed by store id — a flat map, not a nested object. The migration guide's advice follows from that: **every Vuex module, including every nested one, becomes its own store**. ## Naming: the id is the namespace The store **id** plays the role the module namespace played. Keeping ids aligned with the old namespaces makes the migration traceable in devtools and in code review. For a legacy shop with `auth`, `cart` and `catalog` modules: | Vuex namespace | Pinia store id | File | |---|---|---| | `auth` | `auth` | `stores/auth.ts` | | `auth/preferences` | `authPreferences` | `stores/auth-preferences.ts` | | `cart` | `cart` | `stores/cart.ts` | | `catalog` | `catalog` | `stores/catalog.ts` | | `catalog/filters` | `catalogFilters` | `stores/catalog-filters.ts` | | root state and getters | `root` | `stores/root.ts` | Two naming details from the guide: - Use **camelCase** ids. `mapStores()` exposes each store as its id plus `Store`, so `'catalogFilters'` yields `this.catalogFiltersStore`, while a slash id would produce a property reachable only with bracket syntax. - Name the directory **`stores`**, not `store`. It signals many stores, and it lets the Pinia folder sit beside the old Vuex `store/` folder while both run. ## Replacing rootState and rootGetters In Vuex, a module read another module through the extra getter arguments `rootState` and `rootGetters`, or through `rootState` in an action's context. In Pinia: - **Import the other store** and call its `useStore()` function **inside** the getter or action body, then read its properties directly: `useAuthPreferencesStore().currency`. - In a **setup store**, you can call the other store's `useStore()` at the top of the setup function and use it in computed values and functions. - **Nested state access disappears.** `rootState.auth.preferences.currency` becomes a property read on the `authPreferences` store. - **Types follow automatically**, because you read a typed store instead of indexing a string path. - Stores may use each other in both directions, as long as two stores do not each read the other's state at the top of their setup functions; that rule belongs to composing stores, not to the migration. ## Things the flat model removes 1. **Runtime module registration.** Stores are dynamic by design; a store that is never used is never created, so there is nothing to register or unregister. 2. **Namespacing flags.** There is no `namespaced` option; every store is namespaced by its id. 3. **String paths.** Getters and actions are properties and methods on the store object. 4. **A root store object.** Nothing aggregates the stores; the pinia instance only holds them. ## Pitfalls when flattening - **Duplicate ids.** Pinia caches stores by id on the pinia instance, so two `defineStore` calls with the same id — even in different folders — share whichever store was created first. The second definition's state and actions are silently ignored. - **Recreating the tree.** A `root` store that imports and re-exposes every other store rebuilds the coupling the migration was meant to remove; keep it for genuinely global state only. - **Calling `useStore()` at module top level.** Calling another store's `useStore()` outside any function runs at import time, usually before the pinia is installed; call it inside the getter, action or setup function. - **Over-splitting.** A nested module that existed only to group two fields can merge into its parent store; flat does not mean one store per former module at any cost. ## Checking the flattened result After the move, the devtools store list and `pinia.state.value` should show one entry per store id, with names you can map back to the old namespaces at a glance. Every cross-store read should appear as an import at the top of a store file, which turns the implicit `rootState` web of the Vuex tree into an explicit dependency list you can review. If one store imports most of the others, that is the old root module in disguise and a candidate for splitting by feature.

  • What should happen to state and getters that sat at the root of the Vuex store?
    The migration guide suggests moving them into their own store, for example one with the id `root`. It is a normal store like any other; keep it small, because a root store that re-exposes every other store recreates the single tree the migration removes.
  • Why does the migration guide recommend camelCase store ids?
    `mapStores()` builds each property name from the store id plus a `Store` suffix. A camelCase id such as `catalogFilters` gives `this.catalogFiltersStore`, a normal identifier; an id containing a slash would give a property you can only reach with bracket syntax.

saying these in an interview costs you the question

  • Nested Vuex modules must become stores nested inside one parent store.
  • Pinia passes rootState and rootGetters to getters just as Vuex did.
  • Store ids only need to be unique within their own folder.
  • Every store must be registered in main.ts before a component uses it.
  • Flattening loses the namespaces, so store names will inevitably collide.