skip to content

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%

answer

  1. store function, not a namespace
  2. computed versus methods
  3. read-only unless writable
  4. id plus a Store suffix
  5. no helper for mutations

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.

solid answer

~40 s

Pinia's helpers mirror Vuex's, but their first argument is the store definition rather than a namespace string. `mapState(useCartStore, ['items', 'total'])`, or an object with renames and `store => …` functions, gives read-only computed properties for state and getters. `mapWritableState(useCartStore, ['couponCode'])` adds setters so `v-model` works, but takes no functions. `mapActions(useCartStore, ['add'])` goes in `methods`. `mapStores(useCartStore, useAuthStore)` exposes whole stores as `this.cartStore` and `this.authStore` — the id plus a `Store` suffix that `setMapStoreSuffix()` can change — and takes the stores as separate arguments, not an array. `mapGetters` survives only as a deprecated alias of `mapState`, and there is no helper for mutations because they no longer exist.

code

vue · 26 lines
vue
<script lang="ts">
import { defineComponent } from 'vue'
import { mapState, mapWritableState, mapActions, mapStores } from 'pinia'
import { useCartStore } from '@/stores/cart'
import { useAuthStore } from '@/stores/auth'

export default defineComponent({
  computed: {
    ...mapState(useCartStore, ['items', 'total']),
    ...mapState(useCartStore, { itemCount: (store) => store.items.length }),
    ...mapWritableState(useCartStore, ['couponCode']),
    ...mapStores(useAuthStore), // this.authStore
  },
  methods: {
    ...mapActions(useCartStore, { addToCart: 'add', checkout: 'checkout' }),
    async buy() {
      if (this.authStore.loggedIn) await this.checkout()
    },
  },
})
</script>

<template>
  <input v-model="couponCode" />
  <p>{{ itemCount }} items, total {{ total }}</p>
</template>

go deeper

for a junior

Recall which helper goes where: mapState, mapWritableState and mapStores in computed, mapActions in methods, each taking the useStore function.

for a middle

Explain why mapState is read-only, when mapWritableState is needed, and how mapStores derives property names from store ids.

for a senior

Show you can convert a Vuex-era Options API component mechanically, choosing helpers per field and keeping templates unchanged while the store moves.

for a principal

Decide whether map helpers are a bridge or a destination: they let the store move now, and a Composition API rewrite can follow on its own schedule.

## Where map helpers still matter Many Vuex applications were written with the **Options API** — `data`, `computed`, `methods` — and reached the store through Vuex's map helpers, which take a module namespace string. Pinia is designed for `setup()` and `<script setup>`, but it ships a matching set of helpers so Options API components can use stores **without** being rewritten to the Composition API during a migration. That lets a team move a store to Pinia and update its Options API consumers mechanically, leaving any component rewrite for later. Every Pinia helper takes the store's **`useStore` function** — for example `useCartStore` — as its first argument, and calls it with the component's `this.$pinia` internally. There are no namespace strings. ## The four helpers | Helper | Spread into | Gives the component | Notes | |---|---|---|---| | `mapState` | `computed` | read-only state and getters | array of keys, or object with renames and `store => …` functions | | `mapWritableState` | `computed` | state with getters **and setters** | array or rename object; no functions | | `mapActions` | `methods` | actions as methods | array of names, or object `{ localName: 'actionName' }` | | `mapStores` | `computed` | whole stores as `this.<id>Store` | stores passed as separate arguments | ## Read-only versus writable - `mapState` creates **computed properties without setters**. Assigning `this.couponCode = 'SPRING'` to one does not update the store; Vue warns in development that the computed property is readonly. - `mapWritableState` creates computed properties with **both getter and setter**, so `v-model="couponCode"` and `this.couponCode = 'SPRING'` write through to the store. - For **collections**, `mapState` is often enough: `this.items.push(item)` calls a method on the store's reactive array. `mapWritableState` is needed only to replace the whole value, such as `this.items = []`. - `mapState`'s object form accepts functions receiving the store — `itemCount: (store) => store.items.length` — which `mapWritableState` does not. ## mapStores and the id suffix 1. `mapStores(useCartStore, useAuthStore)` adds `this.cartStore` and `this.authStore`: the property name is the store **id plus `Store`**. 2. `setMapStoreSuffix('')` changes the suffix globally — to nothing, or to something like `'_store'`. In TypeScript you must also declare the same suffix by extending the `MapStoresCustomization` interface. 3. Stores are passed **one after another**, not as an array. Passing an array gets a development diagnostic warning that it will fail in production. 4. Because the name comes from the id, **camelCase ids** give clean property names — one reason the migration guide recommends them. ## Converting a component ```js export default { computed: { ...mapState(useCartStore, ['items', 'total']), ...mapWritableState(useCartStore, ['couponCode']), ...mapStores(useCartStore, useAuthStore), }, methods: { ...mapActions(useCartStore, { addToCart: 'add' }), async buy() { if (this.authStore.loggedIn) await this.cartStore.checkout() }, }, } ``` The component's template and method bodies usually stay the same: the mapped names were already `items`, `total` and `couponCode`. What changes is the source of each name and, for fields written through `v-model`, the switch from a read helper to `mapWritableState`. ## Mistakes during conversion - **Passing a namespace string** such as `'cart'` as the first argument, out of Vuex habit; Pinia expects the `useStore` function. - **Putting `mapActions` in `computed`**; actions are methods and belong in `methods`. - **Using `mapState` for a `v-model` field** and wondering why typing does nothing. - **Reaching for `mapGetters`**. It is still exported in Pinia 4, but only as a deprecated alias of `mapState`, kept for migration convenience; getters are mapped with `mapState`. - **Looking for a mutation helper.** Pinia has none; former mutations are now actions, mapped with `mapActions`, or direct writes through `mapWritableState`. ## Bridge or destination Map helpers are a legitimate long-term way to use Pinia from the Options API, not only a migration shim; they are fully typed and supported in Pinia 4. Still, a migration is a good moment to decide per component: - **Keep the helpers** for stable components nobody plans to touch; converting them buys little. - **Move to `setup()` or `<script setup>`** for components under active development, where calling `useCartStore()` directly and destructuring state with `storeToRefs()` reads more naturally. - **Do not mix both styles for one store in one component**; pick one access path so a reader knows where each name comes from. Either way, the store itself does not change: the same Pinia store serves helper-based and Composition API components side by side.

  • In a Pinia Options API component, when do you need mapWritableState for an array rather than mapState?
    Only when the component replaces the whole array, as in `this.items = []`. `mapState` returns the store's reactive array, so calling `this.items.push(item)` already changes store state; only assignment to the mapped property needs a setter.
  • What must a TypeScript project do after calling setMapStoreSuffix('')?
    Declare the same suffix by augmenting Pinia's `MapStoresCustomization` interface, for example `suffix: ''` inside a `declare module 'pinia'` block. Otherwise the types still expect `this.cartStore` while the runtime property is `this.cart`.

saying these in an interview costs you the question

  • Pinia's map helpers take a namespace string like 'cart' as Vuex's did.
  • mapState properties can be assigned, so v-model works with them directly.
  • mapStores should receive the stores wrapped in an array.
  • mapActions belongs in computed next to mapState.
  • mapGetters is required to reach Pinia getters from the Options API.