State & Getters (Pinia)
State & Getters (Pinia)
Level 7 — State Management & Pinia The two fundamental data pillars of a Pinia store: State holds raw reactive data, while Getters hold memoized derived data calculated from State.
1. Prerequisites
- Store (Pinia) — The modular container where State and Getters are defined.
- Computed Properties — The reactivity mechanism that powers Pinia Getters under the hood.
2. Term Category
Vue Ecosystem Construct (Pinia Data Definition & Caching Layer): State and Getters constitute the reactive data layer of a Pinia store. State represents the single source of truth for raw domain values, while Getters represent computed, read-only slices of derived state.
Operating universally across browser rendering engines and SSR environments, State and Getters leverage Vue's ES6 Proxy reactivity. When reactive state properties mutate, dependent Getters evaluate once lazily upon access and cache their results. Subsequent component reads retrieve the cached evaluation instantly without re-executing derivation logic until underlying state dependencies mutate again.
3. Explanation
(1) Design Motivation — "Why did we design this?"
In complex applications, components rarely consume raw state in isolation. An order management screen needs to render calculated subtotals, tax estimates, filtered pending items, and customer status badges. If every component duplicates this calculation math in local methods, performance degrades and business rules diverge across views.
By pairing raw State with memoized Getters directly inside the store, Pinia centralizes domain logic. Getters behave identically to component computed() properties: they track dependencies automatically, cache calculation outputs, and update reactively across all listening components when underlying state variables change. This design prevents duplicate computations and ensures UI components consume a consistent, unified representation of domain state.
(2) Reality Metaphor
Think of State as a raw SQL database table containing raw transaction rows (date, item, amount).
Getters are indexed SQL Views or Materialized Views created on top of that database table. Instead of querying and summing 100,000 raw transaction rows inside every mobile app endpoint or web dashboard view, the database calculates monthly_total_revenue in the Materialized View once. When a user requests the total, the system instantly hands over the pre-calculated cached number. If a new transaction row is inserted into raw State, the Materialized View updates automatically.
(3) Vue Code Examples
Short Snippet
<script setup>
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useStore = defineStore('numbers', () => {
// STATE: Raw reactive data
const list = ref([10, 20, 30])
// GETTER: Derived memoized computation
const sum = computed(() => list.value.reduce((a, b) => a + b, 0))
return { list, sum }
})
</script>
Fuller Example
<script setup>
import { defineStore, storeToRefs } from 'pinia'
import { ref, computed } from 'vue'
// Setup Store defining State and Getters
export const useInventoryStore = defineStore('inventory', () => {
// STATE
const products = ref([
{ id: 1, name: 'Laptop', price: 1200, inStock: true, category: 'tech' },
{ id: 2, name: 'Desk Chair', price: 250, inStock: false, category: 'furniture' },
{ id: 3, name: 'Monitor', price: 400, inStock: true, category: 'tech' }
])
const activeCategory = ref('tech')
// GETTER 1: Filtered product list based on activeCategory state
const filteredProducts = computed(() => {
return products.value.filter(p => p.category === activeCategory.value)
})
// GETTER 2: Dependent on GETTER 1 (Total value of in-stock filtered products)
const categoryTotalValue = computed(() => {
return filteredProducts.value
.filter(p => p.inStock)
.reduce((sum, p) => sum + p.price, 0)
})
function setCategory(cat) {
activeCategory.value = cat
}
return { products, activeCategory, filteredProducts, categoryTotalValue, setCategory }
})
// Component Consumption
const store = useInventoryStore()
const { activeCategory, filteredProducts, categoryTotalValue } = storeToRefs(store)
</script>
<template>
<div class="inventory">
<div class="filter-bar">
<button @click="store.setCategory('tech')">Tech</button>
<button @click="store.setCategory('furniture')">Furniture</button>
</div>
<h3>Category: {{ activeCategory.toUpperCase() }}</h3>
<p>Total Stock Value: ${{ categoryTotalValue }}</p>
<ul>
<li v-for="item in filteredProducts" :key="item.id">
{{ item.name }} - ${{ item.price }} ({{ item.inStock ? 'In Stock' : 'Out of Stock' }})
</li>
</ul>
</div>
</template>
4. Common Mistakes & Pitfalls
Mistake 1: Mutating State directly inside a Getter function
The mistake: Performing mutating operations like array .sort(), .splice(), or assigning properties inside a getter definition.
Why it's wrong: Getters must be pure functions. Array mutators like Array.prototype.sort() mutate the target array in place. Sorting state inside a getter triggers dependency updates, causing an infinite update loop and crashing the browser tab. Always create a shallow copy before sorting ([...state.items].sort()).
Incorrect:
const sortedItems = computed(() => {
return items.value.sort((a, b) => a.price - b.price) // ❌ Mutates raw state array!
})
Fix:
const sortedItems = computed(() => {
return [...items.value].sort((a, b) => a.price - b.price) // Copy array before sorting
})
Mistake 2: Expecting Function-Returning Getters to Cache Results
The mistake: Writing a getter that returns a lookup function getById: (state) => (id) => state.items.find(i => i.id === id) and assuming its output is memoized across invocations.
Why it's wrong: Getters that return a function are not cached! The getter caches the returned inner function reference, but invoking getById(5) executes the inner search loop fresh every single time. For high-frequency rendering, create a computed dictionary or map instead.
Incorrect:
// Function-returning getter runs loop on every render call
const getById = computed(() => (id) => items.value.find(i => i.id === id))
Fix:
// Map-based getter caches O(1) lookup dictionary
const itemMap = computed(() => {
return items.value.reduce((map, item) => {
map[item.id] = item
return map
}, {})
})
Mistake 3: Accessing this inside Arrow Function Options Getters
The mistake: Writing doubleCount: (state) => this.count * 2 inside an Options Store getters configuration block.
Why it's wrong: Arrow functions capture outer lexical scope this (undefined). In Options stores, arrow getters receive state as the first argument, but this is not bound to the store instance.
Incorrect:
getters: {
doubleCount: (state) => this.count * 2 // ❌ 'this' is undefined!
}
Fix:
getters: {
doubleCount: (state) => state.count * 2, // Use explicit state parameter
tripleCount() { return this.count * 3 } // Standard function binds 'this'
}
5. Practice Exercises
Exercise 1: Financial Portfolio Risk Exposure Getters
Scenario: A stock trading application requires a Pinia store tracking asset positions. The store must maintain raw position state and calculate portfolio metrics like total market value, un-hedged risk exposure percentage, and top-performing ticker.
Requirements:
- Define state
positionsarray of{ ticker, shares, purchasePrice, currentPrice, sector }. - Provide getter
totalPortfolioValuesumming current market value of all positions. - Provide getter
techSectorExposurePercentcomputing percentage of portfolio held in'Technology'sector. - Ensure all calculations remain pure without mutating raw positions array.
Answer
Implementation
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const usePortfolioStore = defineStore('portfolioMetrics', () => {
const positions = ref([
{ ticker: 'AAPL', shares: 50, purchasePrice: 150, currentPrice: 180, sector: 'Technology' },
{ ticker: 'JNJ', shares: 100, purchasePrice: 160, currentPrice: 155, sector: 'Healthcare' },
{ ticker: 'NVDA', shares: 20, purchasePrice: 400, currentPrice: 700, sector: 'Technology' }
])
const totalPortfolioValue = computed(() => {
return positions.value.reduce((total, pos) => total + (pos.shares * pos.currentPrice), 0)
})
const techSectorExposurePercent = computed(() => {
if (totalPortfolioValue.value === 0) return 0
const techValue = positions.value
.filter(pos => pos.sector === 'Technology')
.reduce((total, pos) => total + (pos.shares * pos.currentPrice), 0)
return Number(((techValue / totalPortfolioValue.value) * 100).toFixed(2))
})
return { positions, totalPortfolioValue, techSectorExposurePercent }
})
Technical Explanation
- State Independence:
positionsholds primitive numbers and strings as single source of truth. - Getter Chain Dependencies:
techSectorExposurePercentdepends directly ontotalPortfolioValuecomputed getter, demonstrating getter composition. - Pure Function Calculations: Array
.reduce()and.filter()operations create new primitive values without mutating array order or element properties. - Memoization Benefit: Component re-renders do not recalculate exposure percentages unless position prices or share counts update.
Exercise 2: Healthcare Patient Vitals Alert System
Scenario: A hospital intensive care unit monitor requires a Pinia store tracking patient vital signs and triggering prioritized alert getters for nursing staff.
Requirements:
- Maintain state
patientsarray holding{ id, room, heartRate, oxygenSat, status }. - Create getter
criticalPatientsreturning patients withheartRate > 120oroxygenSat < 90. - Create getter
criticalCountreturning length of critical patients list. - Provide action
updateVitals(patientId, newVitals)to mutate patient readings.
Answer
Implementation
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useIcuStore = defineStore('icu', () => {
const patients = ref([
{ id: 'P-101', room: '304-A', heartRate: 75, oxygenSat: 98, status: 'stable' },
{ id: 'P-102', room: '305-B', heartRate: 135, oxygenSat: 88, status: 'warning' }
])
const criticalPatients = computed(() => {
return patients.value.filter(p => p.heartRate > 120 || p.oxygenSat < 90)
})
const criticalCount = computed(() => criticalPatients.value.length)
function updateVitals(patientId, vitals) {
const patient = patients.value.find(p => p.id === patientId)
if (patient) {
Object.assign(patient, vitals)
}
}
return { patients, criticalPatients, criticalCount, updateVitals }
})
Technical Explanation
- Reactive Array Filtering:
criticalPatientsfilters reactive patient proxy objects, auto-updating when vitals mutate. - Derived Count Getter:
criticalCountreadscriticalPatients.value, ensuring reactive propagation across badge indicators. - In-Place Proxy Updates:
Object.assign(patient, vitals)mutates reactive proxy properties directly inside action handler. - Zero Rendering Overhead: Dashboard tables rendering non-critical patients do not execute filtering loops unnecessarily.
Exercise 3: Network Data Pipeline Throughput Metrics
Scenario: A cloud networking monitoring tool needs a store tracking network interface packet counters and deriving bandwidth utilization metrics.
Requirements:
- State
interfacesobject indexed by interface name{ eth0: { rxBytes, txBytes, speedMbps } }. - Getter
totalBandwidthUsageMbpscalculating combined throughput across all interfaces. - Getter
saturatedInterfacesreturning array of interface names operating above 80% capacity. - Action
recordPacketDelta(iface, rxDelta, txDelta)updating byte totals.
Answer
Implementation
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useNetworkStore = defineStore('network', () => {
const interfaces = ref({
eth0: { rxMbps: 450, txMbps: 350, capacityMbps: 1000 },
eth1: { rxMbps: 890, txMbps: 50, capacityMbps: 1000 }
})
const totalBandwidthUsageMbps = computed(() => {
return Object.values(interfaces.value).reduce((sum, iface) => {
return sum + iface.rxMbps + iface.txMbps
}, 0)
})
const saturatedInterfaces = computed(() => {
return Object.entries(interfaces.value)
.filter(([_, iface]) => {
const load = (iface.rxMbps + iface.txMbps) / iface.capacityMbps
return load >= 0.8
})
.map(([name]) => name)
})
function recordMetrics(ifaceName, rxMbps, txMbps) {
if (interfaces.value[ifaceName]) {
interfaces.value[ifaceName].rxMbps = rxMbps
interfaces.value[ifaceName].txMbps = txMbps
}
}
return { interfaces, totalBandwidthUsageMbps, saturatedInterfaces, recordMetrics }
})
Technical Explanation
- Object State Iteration:
Object.valuesandObject.entriesallow computed getters to process dynamic key-value store objects. - Threshold Aggregation:
saturatedInterfacesreturns filtered string array of high-load interface keys for alert components. - Direct Dictionary Assignment: Mutating
rxMbpson targeted interface key updates dependent bandwidth calculations automatically. - Selective Component Re-rendering: Components subscribing only to
saturatedInterfacesignore metric updates on unsaturated links.
6. Related Terms
- Store (Pinia) — The Pinia container where State and Getters are defined.
- Computed Properties — The underlying Vue Composition API primitive powering Getters.
- Actions (Pinia) — The imperative store functions used to mutate State.
- Pinia — The parent state management library.
ref— The primary reactivity wrapper used to define State variables in setup stores.
7. Key Takeaways
- State is the raw, single source of reactive truth in a Pinia store created via
ref()orreactive(). - Getters are derived values calculated from state created using
computed(). - Getters are memoized: they evaluate lazily and cache results until underlying state dependencies mutate.
- Getters must be pure functions; never perform state mutations (
.sort()in-place) inside a getter. - Getters returning parameterized lookup functions are NOT cached; use map/dictionary getters for high-performance lookups.