Documentation Index
Fetch the complete documentation index at: https://docs-mstore.faisalaffan.com/llms.txt
Use this file to discover all available pages before exploring further.
State Management (Pinia)
MStore Dashboard menggunakan Pinia 3.x sebagai state management solution dengan pendekatan composition API.
Overview
State Architecture
Three Levels of State
| Level | Where | When to Use |
|---|
| Global State | Pinia Store | Shared across multiple pages |
| Page State | Composable | Specific to one page |
| Component State | ref/reactive | Local to one component |
Decision Matrix
| Situation | Solution | Example |
|---|
| User authentication | Pinia Store | useAuthStore |
| Shopping cart | Pinia Store | useCartStore |
| Form data | Composable | useInventoryForm |
| Modal open/close | Component ref | const isOpen = ref(false) |
| Search/filter | Composable | useInventoryList |
| Pagination | Composable | usePagination |
Setup
Installation
Pinia sudah terinstall via @pinia/nuxt module:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
})
Persistence Plugin
Untuk persistent state, gunakan pinia-plugin-persistedstate:
// plugins/pinia.ts
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.$pinia.use(piniaPluginPersistedstate)
})
Store Patterns
Basic Store (Composition API)
// features/03_inventory/store/inventory.store.ts
import { defineStore } from 'pinia'
import { getInventoryList, createInventory } from '../api'
import type { Inventory, InventoryQueryParams } from '../types'
export const useInventoryStore = defineStore('inventory', () => {
// State
const items = ref<Inventory[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const total = ref(0)
// Getters
const hasItems = computed(() => items.value.length > 0)
const lowStockItems = computed(() =>
items.value.filter(item => item.stock < item.minStock)
)
// Actions
const fetchItems = async (params?: InventoryQueryParams) => {
loading.value = true
error.value = null
try {
const response = await getInventoryList(params)
items.value = response.data
total.value = response.meta.total
} catch (e) {
error.value = e instanceof Error ? e.message : 'Unknown error'
} finally {
loading.value = false
}
}
const addItem = async (item: Omit<Inventory, 'id'>) => {
const newItem = await createInventory(item)
items.value.push(newItem)
}
// Reset
const $reset = () => {
items.value = []
loading.value = false
error.value = null
total.value = 0
}
return {
// State
items,
loading,
error,
total,
// Getters
hasItems,
lowStockItems,
// Actions
fetchItems,
addItem,
$reset,
}
})
Auth Store with Persistence
// features/01_core/store/auth.ts
import { defineStore } from 'pinia'
import { jwtDecode } from 'jwt-decode'
import type { User, JwtClaims } from '../types'
export const useAuthStore = defineStore('auth', () => {
// State - Access token in memory only (security)
const accessToken = ref<string | null>(null)
const user = ref<User | null>(null)
const isAuthenticated = ref(false)
// Getters
const claims = computed<JwtClaims | null>(() => {
if (!accessToken.value) return null
try {
return jwtDecode<JwtClaims>(accessToken.value)
} catch {
return null
}
})
const hasRole = computed(() => (role: string) => {
return claims.value?.roles?.includes(role) ?? false
})
const hasPermission = computed(() => (permission: string) => {
return claims.value?.permissions?.includes(permission) ?? false
})
// Actions
const setToken = (token: string) => {
accessToken.value = token
isAuthenticated.value = true
}
const setUser = (userData: User) => {
user.value = userData
}
const logout = () => {
accessToken.value = null
user.value = null
isAuthenticated.value = false
}
return {
// State
accessToken,
user,
isAuthenticated,
// Getters
claims,
hasRole,
hasPermission,
// Actions
setToken,
setUser,
logout,
}
}, {
// Persist only user, not token (security)
persist: {
pick: ['user', 'isAuthenticated'],
},
})
Feature Store with API Integration
// features/05_sales/store/sales.store.ts
import { defineStore } from 'pinia'
import { useInventoryStore } from '~/features/03_inventory'
import { createTransaction } from '../api'
import type { CartItem, Transaction } from '../types'
export const useSalesStore = defineStore('sales', () => {
const inventoryStore = useInventoryStore()
// State
const cart = ref<CartItem[]>([])
const transactions = ref<Transaction[]>([])
const currentShift = ref<string | null>(null)
// Getters
const cartTotal = computed(() =>
cart.value.reduce((sum, item) => sum + (item.price * item.quantity), 0)
)
const cartItemCount = computed(() =>
cart.value.reduce((sum, item) => sum + item.quantity, 0)
)
// Actions
const addToCart = (productId: string, quantity: number = 1) => {
// Check stock availability
const product = inventoryStore.items.find(p => p.id === productId)
if (!product || product.stock < quantity) {
throw new Error('Insufficient stock')
}
const existing = cart.value.find(item => item.productId === productId)
if (existing) {
existing.quantity += quantity
} else {
cart.value.push({
productId,
name: product.name,
price: product.price,
quantity,
})
}
}
const checkout = async () => {
const transaction = await createTransaction({
items: cart.value,
total: cartTotal.value,
shiftId: currentShift.value,
})
transactions.value.push(transaction)
cart.value = []
return transaction
}
return {
cart,
transactions,
currentShift,
cartTotal,
cartItemCount,
addToCart,
checkout,
}
})
Composable Patterns
Page Composable
// features/03_inventory/composables/useInventoryList.ts
import { useInventoryStore } from '../store/inventory.store'
import type { InventoryQueryParams } from '../types'
export const useInventoryList = () => {
const store = useInventoryStore()
// Local state for page-specific logic
const searchQuery = ref('')
const currentPage = ref(1)
const pageSize = ref(20)
// Computed from store
const items = computed(() => store.items)
const loading = computed(() => store.loading)
const total = computed(() => store.total)
// Filtered items (local computation)
const filteredItems = computed(() => {
if (!searchQuery.value) return items.value
const query = searchQuery.value.toLowerCase()
return items.value.filter(item =>
item.name.toLowerCase().includes(query) ||
item.sku.toLowerCase().includes(query)
)
})
// Actions
const fetchItems = async () => {
const params: InventoryQueryParams = {
page: currentPage.value,
limit: pageSize.value,
search: searchQuery.value || undefined,
}
await store.fetchItems(params)
}
const goToPage = async (page: number) => {
currentPage.value = page
await fetchItems()
}
// Search with debounce
const debouncedSearch = useDebounceFn(() => {
currentPage.value = 1
fetchItems()
}, 300)
watch(searchQuery, () => {
debouncedSearch()
})
return {
// State
searchQuery,
currentPage,
pageSize,
// Computed
items: filteredItems,
loading,
total,
// Actions
fetchItems,
goToPage,
}
}
// features/03_inventory/composables/useInventoryForm.ts
import { useInventoryStore } from '../store/inventory.store'
import type { Inventory } from '../types'
export const useInventoryForm = (initialData?: Inventory) => {
const store = useInventoryStore()
// Form state
const form = reactive({
name: initialData?.name ?? '',
sku: initialData?.sku ?? '',
price: initialData?.price ?? 0,
stock: initialData?.stock ?? 0,
minStock: initialData?.minStock ?? 0,
category: initialData?.category ?? '',
})
// Validation
const errors = reactive({
name: '',
sku: '',
price: '',
})
const isValid = computed(() => {
return !Object.values(errors).some(e => e !== '')
})
// Validate
const validate = () => {
errors.name = form.name.length < 2 ? 'Name must be at least 2 characters' : ''
errors.sku = form.sku.length < 3 ? 'SKU must be at least 3 characters' : ''
errors.price = form.price <= 0 ? 'Price must be greater than 0' : ''
return isValid.value
}
// Submit
const submit = async () => {
if (!validate()) return false
try {
if (initialData?.id) {
await store.updateItem(initialData.id, form)
} else {
await store.addItem(form)
}
return true
} catch (error) {
console.error('Form submission failed:', error)
return false
}
}
// Reset
const reset = () => {
Object.assign(form, {
name: initialData?.name ?? '',
sku: initialData?.sku ?? '',
price: initialData?.price ?? 0,
stock: initialData?.stock ?? 0,
minStock: initialData?.minStock ?? 0,
category: initialData?.category ?? '',
})
Object.keys(errors).forEach(key => {
errors[key as keyof typeof errors] = ''
})
}
return {
form,
errors,
isValid,
validate,
submit,
reset,
}
}
Using in Components
In Page
<!-- app/pages/inventory/index.vue -->
<script setup lang="ts">
import { useInventoryList } from '~/features/03_inventory'
const {
items,
loading,
searchQuery,
currentPage,
total,
fetchItems,
goToPage,
} = useInventoryList()
onMounted(() => {
fetchItems()
})
</script>
<template>
<div>
<UInput v-model="searchQuery" placeholder="Search..." />
<InventoryTable
:items="items"
:loading="loading"
/>
<UPagination
:total="total"
:page="currentPage"
@update:page="goToPage"
/>
</div>
</template>
In Component
<!-- features/03_inventory/components/InventoryTable.vue -->
<script setup lang="ts">
import { useInventoryStore } from '../store/inventory.store'
defineProps<{
items: Inventory[]
loading: boolean
}>()
const store = useInventoryStore()
const deleteItem = async (id: string) => {
if (confirm('Are you sure?')) {
await store.deleteItem(id)
}
}
</script>
Persistence
Full Store Persistence
export const useSettingsStore = defineStore('settings', () => {
const theme = ref<'light' | 'dark'>('light')
const language = ref('id')
return { theme, language }
}, {
persist: true, // Persist entire store
})
Partial Persistence
export const useAuthStore = defineStore('auth', () => {
const token = ref<string | null>(null) // Don't persist (security)
const user = ref<User | null>(null) // Persist
return { token, user }
}, {
persist: {
pick: ['user'], // Only persist user
},
})
Custom Storage
export const useCartStore = defineStore('cart', () => {
// ...
}, {
persist: {
storage: sessionStorage, // Use sessionStorage instead
},
})
Best Practices
- One store per domain
- Keep stores focused and small
- Use composables for page-specific logic
- Use computed for derived state
- Never persist sensitive data (tokens, passwords)
- Use memory-only for access tokens
- Validate data before storing
- Clear sensitive data on logout
// tests/unit/store/inventory.test.ts
import { setActivePinia, createPinia } from 'pinia'
import { useInventoryStore } from '~/features/03_inventory'
describe('Inventory Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('should add item to cart', () => {
const store = useInventoryStore()
store.addItem({ name: 'Test', sku: 'TST001', price: 100 })
expect(store.items).toHaveLength(1)
})
})
Next Steps
Data Flow
Complete data flow patterns
API Integration
API fetching dengan auto-refresh