skip to content

Moving From Vuex

Pinia replaced Vuex as Vue's recommended store: no mutations, flat id-named stores and real type inference. Interviewers ask why mutations vanished and how to move a Vuex module over.

on this pageshow

explore

questions

5

In a Vue 3 app, what are the main differences between Pinia and Vuex 4, and why is Pinia now the recommended store?

level: juniorimportance: must knowfreq 62%

answer

  1. one store vs many stores
  2. the commit layer is gone
  3. the id doubles as the namespace
  4. imported functions, not string keys
  5. Vuex is in maintenance mode

basics

~20 s

Pinia drops Vuex's mutations, replaces one store of nested namespaced modules with flat stores named by id, and infers types from the store definition instead of string keys. Vuex is in maintenance mode; Pinia is the official recommendation.

solid answer

~50 s

Vuex 4 gives an app one store built from modules: state changes go through `commit` to synchronous mutations, async work lives in actions, and components reach getters and actions by string paths such as `'cart/total'`. Pinia replaces that with many small stores, each created by `defineStore(id, …)` and used by calling its `useCartStore()`-style function. There are no mutations: actions can be sync or async, state can be written directly or with `$patch`, and devtools still records those writes. The id acts as the namespace, a store registers itself the first time it is used, and because you import functions instead of strings, TypeScript infers state, getters and actions without hand-written wrappers. Vue's documentation puts Vuex in maintenance mode — it still works on Vue 3 but gets no new features — and recommends Pinia for new applications.

code

ts · 15 lines
ts
// Vuex 4: the cart module inside one store, reached by string paths
const cart = {
  namespaced: true,
  state: () => ({ items: [] as CartItem[] }),
  getters: { count: (state) => state.items.length },
  mutations: {
    add(state, item: CartItem) { state.items.push(item) },
  },
  actions: {
    async addById({ commit }, id: number) {
      commit('add', await api.items.load(id))
    },
  },
}
// in a component: store.getters['cart/count']

go deeper

for a junior

Recall the headline: Pinia has no mutations, uses many flat stores named by id, and infers types; Vuex is in maintenance mode and Pinia is the recommendation.

for a middle

Explain why mutations and nested modules could be dropped: devtools records direct writes itself, and stores import each other instead of reading rootState paths.

for a senior

Show you can judge a live Vuex 4 codebase: it still works on Vue 3, so moving is a cost-benefit decision taken store by store, not an emergency.

for a principal

Frame adoption as risk against payoff: typed flat stores lower long-term friction, but a working Vuex 4 app can wait until the team touches those modules anyway.

## Two libraries for one job **Vuex** was Vue's original official state-management library: Vuex 3 targets Vue 2, and Vuex 4 targets Vue 3 with largely the same API. **Pinia** began as an experiment in what a Vuex 5 could look like; the Vue team found it already did most of what Vuex 5 was meant to do and made it the recommendation instead. Vue's own documentation now describes Vuex as being in **maintenance mode**: it still works, it will not receive new features, and new applications are pointed at Pinia. That status matters in an interview. A Vuex 4 application on Vue 3 is **not broken** and is not forced to move; the reasons to move are the ones below, weighed against the cost of converting code that already works. Both libraries solve the same problem — state that outlives a single component and is shared across the component tree — so the real question is not "why use a store" but "what did Pinia change about the store". ## What changes, side by side | Concern | Vuex 4 | Pinia 4 | |---|---|---| | Shape | one store assembled from `modules`, optionally nested | many independent stores, each from `defineStore(id, …)` | | Namespacing | `namespaced: true` per module, string paths like `'cart/total'` | the store `id` is the namespace; every store is namespaced by design | | Changing state | synchronous **mutations** invoked through `commit` | no mutations: direct writes, `$patch`, or actions | | Async work | actions that receive a `context` object and then commit | actions are plain methods, sync or async, using `this` | | Reaching the store | `useStore()` from `vuex`, then string keys | import `useCartStore()` and read properties | | Registration | modules declared up front or registered at runtime | a store is created the first time its `useStore()` runs | | TypeScript | declared state interfaces and casts around string keys | state, getters and actions inferred from the definition | ## Why the removed pieces could go - **Mutations** existed largely so devtools could record every change. Pinia's devtools integration tracks direct writes and `$patch()` calls too, and both can be time-travelled, so a separate synchronous layer only added ceremony. - **Nested modules** made other modules reachable only through paths such as `rootState.auth.preferences`. Pinia keeps stores flat; a store that needs another imports it and calls its `useXStore()` function. - **Magic strings** — getter and action names written as `'module/name'` keys — are replaced by imported functions and properties that the editor autocompletes and the compiler checks. - **Runtime module registration** has no counterpart because nothing needs it: a store that is never used is never created, and one that is used is created and cached by its id on the pinia instance. ## TypeScript: inference instead of declarations In an **option store**, the return type of `state()` becomes the state type, arrow-function getters infer their return type from the expression, and actions are typed from their own signatures. A **setup store** infers everything from what its function returns. The known gap is a getter written as a regular function that reads `this`: TypeScript needs an explicit return type there. Compared with Vuex, where each module's state type and every string path had to be declared or cast by hand, a converted store usually needs fewer annotations. A useful side effect during a migration is that type errors point straight at call sites still using the old shape. ## What stays familiar - The three concepts — **state**, **getters**, **actions** — keep their names, and an option store is laid out almost exactly like a Vuex module without its `mutations` block. - Options API components keep **map helpers**: `mapState`, `mapWritableState`, `mapActions` and `mapStores`. - Devtools, plugins, hot module replacement and server-side rendering are all supported, through Pinia's own APIs rather than Vuex's. ## Version notes worth saying out loud 1. **Pinia 3** removed Vue 2 support and the old `defineStore({ id: … })` object form; the only remaining forms are `defineStore(id, options)` and `defineStore(id, setupFn, options?)`. 2. **Pinia 4** (4.0.0, July 2026) is ESM-only and requires `@vue/devtools-api` to be installed as a peer dependency; its dev warnings are coded diagnostics. 3. **Vuex 4** remains the Vuex line for Vue 3 and is in maintenance mode. A candidate who describes Pinia with the object-with-id form, or who claims Vuex cannot run on Vue 3, is describing an API or a constraint that no longer matches reality.

  • Does Pinia still support Options API components that used Vuex's map helpers?
    Yes. `mapStores`, `mapState`, `mapWritableState` and `mapActions` let Options API components use stores without `setup()`. The first argument is the store's `useStore` function rather than a namespace string, and there is no mutation helper because mutations no longer exist.
  • If nothing registers a Pinia store up front, when is it created?
    The first time its `useStore()` function runs with an active pinia, Pinia builds the store and caches it by id on that pinia instance; later calls return the same object. That is why Pinia needs no runtime module registration, and why two definitions that share an id end up sharing one store.

saying these in an interview costs you the question

  • Pinia is just Vuex 5 with the same API renamed.
  • Pinia still needs mutations so devtools can see state changes.
  • Vuex 4 cannot run on Vue 3, so migrating is mandatory.
  • Pinia stores must be listed in a root modules option before use.
  • Pinia's TypeScript support needs a hand-written root state interface.
open as a page

Why does Pinia have no mutations when Vuex routed every state change through commit, and what replaced them?

level: middleimportance: must knowfreq 52%

basics

~10 s

Vuex mutations existed so devtools could record each synchronous state change. Pinia's devtools integration records direct writes and $patch calls itself, so mutations were dropped; state now changes through actions, direct assignment or $patch.

open as a page

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%

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.

open as a page

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%

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.

open as a page

In Options API components moving from Vuex to Pinia, how do mapStores, mapState, mapWritableState and mapActions replace Vuex's map helpers?

level: seniorimportance: nice to knowfreq 24%

basics

~10 s

Spread mapStores, mapState and mapWritableState into computed and mapActions into methods, passing the store's useStore function instead of a Vuex namespace string. mapState is read-only, mapWritableState adds setters, and nothing maps mutations.

open as a page