Skip to content

mapState()

Maps store state properties and getters to computed properties in Options API components. Provides direct access to state without needing the store instance.

Signature

ts
function mapState<T>(
  useStore: () => T,
  keys: (keyof T)[] | Record<string, keyof T | ((store: T) => any)>
): ComputedOptions

function mapState<T>(
  useStore: () => T,
  keysAndMapper: Record<string, string | ((store: T) => any)>
): ComputedOptions

Parameters

  • useStore: Store definition function
  • keys: Array of state property names or object with custom mappings
  • keysAndMapper: Object mapping custom names to state properties or getter functions

Returns

Computed properties object for Options API components.

Basic Usage

Array Syntax

js
import { mapState } from 'pinia'
import { useCounterStore } from '@/stores/counter'

export default {
  computed: {
    // Maps this.count, this.doubleCount from store
    ...mapState(useCounterStore, ['count', 'doubleCount'])
  },
  
  template: `
    <div>
      <p>Count: {{ count }}</p>
      <p>Double: {{ doubleCount }}</p>
    </div>
  `
}

Object Syntax

js
import { mapState } from 'pinia'
import { useUserStore } from '@/stores/user'

export default {
  computed: {
    // Custom property names
    ...mapState(useUserStore, {
      userName: 'name',
      userEmail: 'email',
      isLoggedIn: 'authenticated'
    })
  },
  
  template: `
    <div v-if="isLoggedIn">
      <h1>Welcome {{ userName }}</h1>
      <p>Email: {{ userEmail }}</p>
    </div>
  `
}

Function Syntax

js
import { mapState } from 'pinia'
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'

export default {
  computed: {
    ...mapState(useUserStore, {
      // Access state directly
      userName: 'name',
      
      // Use getter function for computed values
      userInfo: (store) => `${store.name} (${store.email})`,
      
      // Access nested properties
      userPreferences: (store) => store.profile.preferences,
      
      // Combine with other stores
      userSummary: (store) => {
        const cartStore = useCartStore()
        return {
          name: store.name,
          cartItems: cartStore.items.length
        }
      }
    })
  }
}

State vs Getters

Mapping State Properties

js
// Store definition
export const useProductStore = defineStore('product', {
  state: () => ({
    products: [],
    selectedProduct: null,
    filters: {
      category: '',
      priceRange: [0, 1000]
    }
  }),
  
  getters: {
    filteredProducts: (state) => {
      return state.products.filter(product => {
        return product.category.includes(state.filters.category)
      })
    },
    
    selectedProductPrice: (state) => {
      return state.selectedProduct?.price || 0
    }
  }
})

// Component
export default {
  computed: {
    // Map state properties
    ...mapState(useProductStore, [
      'products',
      'selectedProduct',
      'filters'
    ]),
    
    // Map getters
    ...mapState(useProductStore, [
      'filteredProducts',
      'selectedProductPrice'
    ])
  }
}

Nested State Access

js
import { mapState } from 'pinia'
import { useSettingsStore } from '@/stores/settings'

export default {
  computed: {
    ...mapState(useSettingsStore, {
      // Direct nested access
      theme: (store) => store.ui.theme,
      language: (store) => store.ui.language,
      
      // Complex nested access
      notificationSettings: (store) => store.user.preferences.notifications,
      
      // Computed from nested state
      isDarkMode: (store) => store.ui.theme === 'dark',
      
      // Multiple nested properties
      uiConfig: (store) => ({
        theme: store.ui.theme,
        layout: store.ui.layout,
        sidebar: store.ui.sidebar
      })
    })
  }
}

Advanced Usage

Conditional Mapping

js
import { mapState } from 'pinia'
import { useUserStore } from '@/stores/user'
import { useAdminStore } from '@/stores/admin'

export default {
  computed: {
    ...mapState(useUserStore, ['name', 'role']),
    
    // Conditional mapping based on user role
    ...mapState(useUserStore, {
      adminData: (store) => {
        if (store.role === 'admin') {
          const adminStore = useAdminStore()
          return adminStore.adminData
        }
        return null
      }
    })
  }
}

Multiple Store Mapping

js
import { mapState } from 'pinia'
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
import { useProductStore } from '@/stores/product'

export default {
  computed: {
    // User store
    ...mapState(useUserStore, ['name', 'email']),
    
    // Cart store
    ...mapState(useCartStore, {
      cartItemCount: 'itemCount',
      cartTotal: 'total'
    }),
    
    // Product store
    ...mapState(useProductStore, {
      availableProducts: 'products',
      selectedProduct: 'currentProduct'
    }),
    
    // Combined computed property
    userCartSummary() {
      return {
        user: this.name,
        items: this.cartItemCount,
        total: this.cartTotal,
        selectedProduct: this.selectedProduct?.name
      }
    }
  }
}

Dynamic State Access

js
import { mapState } from 'pinia'
import { useDataStore } from '@/stores/data'

export default {
  props: ['entityType'],
  
  computed: {
    ...mapState(useDataStore, {
      // Dynamic property access based on props
      entities: (store) => store[this.entityType] || [],
      
      // Dynamic getter access
      filteredEntities: (store) => {
        const getterName = `filtered${this.entityType.charAt(0).toUpperCase() + this.entityType.slice(1)}`
        return store[getterName] || []
      },
      
      // Dynamic computed properties
      entityCount: (store) => (store[this.entityType] || []).length
    })
  }
}

TypeScript

Type Safety

ts
import { mapState } from 'pinia'
import { useUserStore } from '@/stores/user'
import type { ComputedOptions } from 'vue'

interface ComponentComputed {
  userName: string
  userEmail: string
  isAuthenticated: boolean
  userProfile: {
    name: string
    email: string
    preferences: Record<string, any>
  }
}

export default defineComponent({
  computed: {
    ...mapState(useUserStore, {
      userName: 'name',
      userEmail: 'email',
      isAuthenticated: 'authenticated',
      userProfile: (store) => ({
        name: store.name,
        email: store.email,
        preferences: store.preferences
      })
    })
  } as ComputedOptions<ComponentComputed>
})

Generic State Mapping

ts
function createStateMapper<T extends Record<string, any>>(
  useStore: () => T,
  keys: (keyof T)[]
) {
  return mapState(useStore, keys)
}

// Usage
const userStateMap = createStateMapper(useUserStore, ['name', 'email', 'authenticated'])

export default {
  computed: {
    ...userStateMap
  }
}

Typed Store Access

ts
import { mapState } from 'pinia'
import type { UserStore } from '@/stores/user'

interface MappedState {
  userName: string
  userEmail: string
  fullUserInfo: string
}

export default defineComponent({
  computed: {
    ...mapState(useUserStore, {
      userName: 'name',
      userEmail: 'email',
      fullUserInfo: (store: UserStore) => `${store.name} <${store.email}>`
    } as Record<keyof MappedState, keyof UserStore | ((store: UserStore) => any)>)
  }
})

Performance Optimization

Selective Mapping

js
import { mapState } from 'pinia'
import { useLargeDataStore } from '@/stores/largeData'

export default {
  computed: {
    // Only map what you need
    ...mapState(useLargeDataStore, {
      // Instead of mapping entire large object
      // largeData: 'data',
      
      // Map only specific properties
      dataLength: (store) => store.data.length,
      firstItem: (store) => store.data[0],
      lastItem: (store) => store.data[store.data.length - 1]
    })
  }
}

Memoized Computations

js
import { mapState } from 'pinia'
import { useExpensiveStore } from '@/stores/expensive'

export default {
  computed: {
    ...mapState(useExpensiveStore, {
      // Memoize expensive computations
      expensiveComputation: (store) => {
        // Use a getter that's already memoized in the store
        return store.memoizedExpensiveGetter
      },
      
      // Or implement simple caching
      cachedResult: (store) => {
        const cacheKey = `${store.param1}-${store.param2}`
        if (!this._cache || this._cacheKey !== cacheKey) {
          this._cache = store.computeExpensiveResult()
          this._cacheKey = cacheKey
        }
        return this._cache
      }
    })
  }
}

Common Patterns

Form Binding

js
import { mapState } from 'pinia'
import { useFormStore } from '@/stores/form'

export default {
  computed: {
    ...mapState(useFormStore, {
      // Form field values
      formData: 'data',
      formErrors: 'errors',
      isValid: 'valid',
      
      // Computed form state
      canSubmit: (store) => store.valid && !store.submitting,
      errorCount: (store) => Object.keys(store.errors).length
    })
  },
  
  template: `
    <form @submit.prevent="submitForm">
      <input v-model="formData.name" :class="{ error: formErrors.name }">
      <p v-if="formErrors.name">{{ formErrors.name }}</p>
      
      <button :disabled="!canSubmit">Submit</button>
      <p v-if="errorCount > 0">{{ errorCount }} errors found</p>
    </form>
  `
}

List Rendering

js
import { mapState } from 'pinia'
import { useItemsStore } from '@/stores/items'

export default {
  computed: {
    ...mapState(useItemsStore, {
      items: 'items',
      loading: 'loading',
      
      // Computed list properties
      itemCount: (store) => store.items.length,
      hasItems: (store) => store.items.length > 0,
      
      // Filtered/sorted items
      sortedItems: (store) => {
        return [...store.items].sort((a, b) => a.name.localeCompare(b.name))
      },
      
      // Grouped items
      groupedItems: (store) => {
        return store.items.reduce((groups, item) => {
          const group = item.category
          groups[group] = groups[group] || []
          groups[group].push(item)
          return groups
        }, {})
      }
    })
  },
  
  template: `
    <div>
      <p v-if="loading">Loading...</p>
      <p v-else-if="!hasItems">No items found</p>
      <div v-else>
        <p>Total items: {{ itemCount }}</p>
        <div v-for="(items, category) in groupedItems" :key="category">
          <h3>{{ category }}</h3>
          <ul>
            <li v-for="item in items" :key="item.id">
              {{ item.name }}
            </li>
          </ul>
        </div>
      </div>
    </div>
  `
}

Conditional Rendering

js
import { mapState } from 'pinia'
import { useAuthStore } from '@/stores/auth'
import { useUIStore } from '@/stores/ui'

export default {
  computed: {
    ...mapState(useAuthStore, {
      user: 'currentUser',
      isAuthenticated: 'authenticated',
      userRole: (store) => store.currentUser?.role
    }),
    
    ...mapState(useUIStore, {
      showSidebar: 'sidebarVisible',
      theme: 'currentTheme'
    }),
    
    // Conditional display logic
    showAdminPanel() {
      return this.isAuthenticated && this.userRole === 'admin'
    },
    
    showUserMenu() {
      return this.isAuthenticated && this.showSidebar
    },
    
    cssClasses() {
      return {
        'theme-dark': this.theme === 'dark',
        'sidebar-open': this.showSidebar,
        'user-authenticated': this.isAuthenticated
      }
    }
  }
}

Migration from Vuex

Vuex mapState

js
// Vuex
import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState({
      count: state => state.count,
      countAlias: 'count',
      countPlusLocalState(state) {
        return state.count + this.localCount
      }
    })
  }
}

// Pinia equivalent
import { mapState } from 'pinia'
import { useCounterStore } from '@/stores/counter'

export default {
  computed: {
    ...mapState(useCounterStore, {
      count: 'count',
      countAlias: 'count',
      countPlusLocalState: (store) => store.count + this.localCount
    })
  }
}

Vuex Namespaced Modules

js
// Vuex namespaced
import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState('user', {
      name: 'name',
      email: 'email'
    }),
    ...mapState('cart', {
      items: 'items',
      total: 'total'
    })
  }
}

// Pinia equivalent
import { mapState } from 'pinia'
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'

export default {
  computed: {
    ...mapState(useUserStore, {
      name: 'name',
      email: 'email'
    }),
    ...mapState(useCartStore, {
      items: 'items',
      total: 'total'
    })
  }
}

Best Practices

1. Use Descriptive Names

js
// ✅ Good - clear property names
computed: {
  ...mapState(useUserStore, {
    currentUserName: 'name',
    currentUserEmail: 'email',
    isCurrentUserAdmin: (store) => store.role === 'admin'
  })
}

// ❌ Confusing - unclear names
computed: {
  ...mapState(useUserStore, {
    n: 'name',
    e: 'email',
    a: (store) => store.role === 'admin'
  })
}
js
// ✅ Good - logical grouping
computed: {
  // User information
  ...mapState(useUserStore, {
    userName: 'name',
    userEmail: 'email',
    userRole: 'role'
  }),
  
  // UI state
  ...mapState(useUIStore, {
    sidebarOpen: 'sidebarVisible',
    currentTheme: 'theme',
    loading: 'isLoading'
  })
}

3. Prefer Getters for Computed Values

js
// ✅ Good - use store getters
computed: {
  ...mapState(useProductStore, [
    'filteredProducts', // This is a getter
    'sortedProducts',   // This is a getter
    'productCount'      // This is a getter
  ])
}

// ❌ Less efficient - compute in component
computed: {
  ...mapState(useProductStore, {
    filteredProducts: (store) => {
      // This computation happens in every component instance
      return store.products.filter(p => p.active)
    }
  })
}

4. Handle Undefined State

js
computed: {
  ...mapState(useUserStore, {
    userName: (store) => store.user?.name || 'Guest',
    userAvatar: (store) => store.user?.avatar || '/default-avatar.png',
    userPreferences: (store) => store.user?.preferences || {}
  })
}

See Also

Released under the MIT License.