mapWritableState()
Maps store state properties to writable computed properties in Options API components. Unlike mapState()
, this allows direct mutation of state properties from the component.
Signature
ts
function mapWritableState<T>(
useStore: () => T,
keys: (keyof T)[] | Record<string, keyof T>
): WritableComputedOptions
Parameters
- useStore: Store definition function
- keys: Array of state property names or object with custom mappings
Returns
Writable computed properties object for Options API components.
Basic Usage
Array Syntax
js
import { mapWritableState } from 'pinia'
import { useCounterStore } from '@/stores/counter'
export default {
computed: {
// Maps this.count, this.name as writable computed properties
...mapWritableState(useCounterStore, ['count', 'name'])
},
methods: {
increment() {
this.count++ // Direct mutation
},
updateName(newName) {
this.name = newName // Direct assignment
}
},
template: `
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+</button>
<input v-model="name" placeholder="Enter name">
</div>
`
}
Object Syntax
js
import { mapWritableState } from 'pinia'
import { useUserStore } from '@/stores/user'
export default {
computed: {
// Custom property names
...mapWritableState(useUserStore, {
userName: 'name',
userEmail: 'email',
userAge: 'age'
})
},
methods: {
updateProfile(profile) {
this.userName = profile.name
this.userEmail = profile.email
this.userAge = profile.age
}
},
template: `
<form @submit.prevent="saveProfile">
<input v-model="userName" placeholder="Name">
<input v-model="userEmail" placeholder="Email">
<input v-model.number="userAge" placeholder="Age">
<button type="submit">Save</button>
</form>
`
}
Form Binding
Two-Way Data Binding
js
import { mapWritableState } from 'pinia'
import { useFormStore } from '@/stores/form'
export default {
computed: {
...mapWritableState(useFormStore, [
'firstName',
'lastName',
'email',
'phone',
'address'
])
},
template: `
<form>
<div class="form-group">
<label>First Name:</label>
<input v-model="firstName" type="text">
</div>
<div class="form-group">
<label>Last Name:</label>
<input v-model="lastName" type="text">
</div>
<div class="form-group">
<label>Email:</label>
<input v-model="email" type="email">
</div>
<div class="form-group">
<label>Phone:</label>
<input v-model="phone" type="tel">
</div>
<div class="form-group">
<label>Address:</label>
<textarea v-model="address"></textarea>
</div>
</form>
`
}
Complex Form State
js
import { mapWritableState } from 'pinia'
import { useSettingsStore } from '@/stores/settings'
export default {
computed: {
...mapWritableState(useSettingsStore, {
darkMode: 'theme.dark',
language: 'locale.language',
notifications: 'preferences.notifications',
autoSave: 'preferences.autoSave'
})
},
template: `
<div class="settings-panel">
<div class="setting-item">
<label>
<input v-model="darkMode" type="checkbox">
Dark Mode
</label>
</div>
<div class="setting-item">
<label>Language:</label>
<select v-model="language">
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
</select>
</div>
<div class="setting-item">
<label>
<input v-model="notifications" type="checkbox">
Enable Notifications
</label>
</div>
<div class="setting-item">
<label>
<input v-model="autoSave" type="checkbox">
Auto Save
</label>
</div>
</div>
`
}
Advanced Usage
Nested State Properties
js
import { mapWritableState } from 'pinia'
import { useProfileStore } from '@/stores/profile'
// Store definition
export const useProfileStore = defineStore('profile', {
state: () => ({
user: {
personal: {
name: '',
email: '',
avatar: ''
},
preferences: {
theme: 'light',
language: 'en',
notifications: true
}
}
})
})
// Component
export default {
computed: {
// Note: mapWritableState works best with flat state properties
// For nested properties, consider using store actions or direct store access
...mapWritableState(useProfileStore, {
// These would need to be flattened in the store or accessed differently
userName: (store) => store.user.personal.name,
userEmail: (store) => store.user.personal.email
})
},
methods: {
// Better approach for nested state
updatePersonalInfo(info) {
const store = useProfileStore()
store.user.personal = { ...store.user.personal, ...info }
}
}
}
Conditional Writable State
js
import { mapWritableState } from 'pinia'
import { useUserStore } from '@/stores/user'
import { useAdminStore } from '@/stores/admin'
export default {
computed: {
...mapWritableState(useUserStore, ['name', 'email']),
// Conditionally writable based on user permissions
adminSettings: {
get() {
const userStore = useUserStore()
if (userStore.isAdmin) {
const adminStore = useAdminStore()
return adminStore.settings
}
return null
},
set(value) {
const userStore = useUserStore()
if (userStore.isAdmin) {
const adminStore = useAdminStore()
adminStore.settings = value
}
}
}
}
}
Array and Object State
js
import { mapWritableState } from 'pinia'
import { useListStore } from '@/stores/list'
export default {
computed: {
...mapWritableState(useListStore, ['items', 'filters'])
},
methods: {
addItem(item) {
this.items.push(item) // Direct array mutation
},
removeItem(index) {
this.items.splice(index, 1) // Direct array mutation
},
updateFilter(key, value) {
this.filters[key] = value // Direct object mutation
},
clearFilters() {
this.filters = {} // Direct object replacement
}
},
template: `
<div>
<div class="filters">
<input
v-model="filters.search"
placeholder="Search..."
>
<select v-model="filters.category">
<option value="">All Categories</option>
<option value="tech">Technology</option>
<option value="design">Design</option>
</select>
</div>
<ul class="items">
<li v-for="(item, index) in items" :key="item.id">
{{ item.name }}
<button @click="removeItem(index)">Remove</button>
</li>
</ul>
<button @click="addItem({ id: Date.now(), name: 'New Item' })">
Add Item
</button>
</div>
`
}
TypeScript
Type Safety
ts
import { mapWritableState } from 'pinia'
import { useCounterStore } from '@/stores/counter'
import type { WritableComputedOptions } from 'vue'
interface ComponentComputed {
count: number
name: string
isActive: boolean
}
export default defineComponent({
computed: {
...mapWritableState(useCounterStore, [
'count',
'name',
'isActive'
])
} as WritableComputedOptions<ComponentComputed>
})
Generic Writable State Mapping
ts
function createWritableStateMapper<
T extends Record<string, any>,
K extends keyof T
>(
useStore: () => T,
keys: K[]
): WritableComputedOptions<Pick<T, K>> {
return mapWritableState(useStore, keys)
}
// Usage
const writableUserState = createWritableStateMapper(
useUserStore,
['name', 'email', 'age']
)
export default {
computed: {
...writableUserState
}
}
Comparison with Other Mapping Functions
vs mapState
js
// mapState - read-only access
computed: {
...mapState(useCounterStore, ['count']),
// This won't work - computed properties are read-only by default
// this.count = 10 // Error!
}
// mapWritableState - read-write access
computed: {
...mapWritableState(useCounterStore, ['count']),
// This works - writable computed properties
// this.count = 10 // ✅ Works!
}
vs Direct Store Access
js
// Direct store access
export default {
computed: {
count: {
get() {
const store = useCounterStore()
return store.count
},
set(value) {
const store = useCounterStore()
store.count = value
}
}
}
}
// mapWritableState - equivalent but more concise
export default {
computed: {
...mapWritableState(useCounterStore, ['count'])
}
}
Performance Considerations
Selective Mapping
js
// ✅ Good - only map what you need to modify
computed: {
...mapWritableState(useFormStore, [
'firstName', // Will be modified
'lastName', // Will be modified
'email' // Will be modified
]),
// Use mapState for read-only properties
...mapState(useFormStore, [
'isValid', // Read-only
'errors', // Read-only
'submitting' // Read-only
])
}
// ❌ Less efficient - mapping everything as writable
computed: {
...mapWritableState(useFormStore, [
'firstName', 'lastName', 'email',
'isValid', 'errors', 'submitting' // These don't need to be writable
])
}
Batch Updates
js
import { mapWritableState } from 'pinia'
import { useFormStore } from '@/stores/form'
export default {
computed: {
...mapWritableState(useFormStore, [
'firstName',
'lastName',
'email'
])
},
methods: {
// ✅ Good - batch updates
updateProfile(profile) {
// Use store action for batch updates
const store = useFormStore()
store.updateProfile(profile)
},
// ❌ Less efficient - individual updates
updateProfileIndividually(profile) {
this.firstName = profile.firstName // Triggers reactivity
this.lastName = profile.lastName // Triggers reactivity
this.email = profile.email // Triggers reactivity
}
}
}
Common Patterns
Form Validation
js
import { mapWritableState } from 'pinia'
import { useFormStore } from '@/stores/form'
export default {
computed: {
...mapWritableState(useFormStore, [
'email',
'password',
'confirmPassword'
]),
...mapState(useFormStore, [
'errors',
'isValid'
])
},
watch: {
email(newEmail) {
// Validate email when it changes
const store = useFormStore()
store.validateEmail(newEmail)
},
password(newPassword) {
// Validate password when it changes
const store = useFormStore()
store.validatePassword(newPassword)
}
},
template: `
<form>
<div>
<input
v-model="email"
type="email"
:class="{ error: errors.email }"
>
<span v-if="errors.email">{{ errors.email }}</span>
</div>
<div>
<input
v-model="password"
type="password"
:class="{ error: errors.password }"
>
<span v-if="errors.password">{{ errors.password }}</span>
</div>
<button :disabled="!isValid">Submit</button>
</form>
`
}
Settings Panel
js
import { mapWritableState } from 'pinia'
import { useSettingsStore } from '@/stores/settings'
export default {
computed: {
...mapWritableState(useSettingsStore, [
'theme',
'language',
'notifications',
'autoSave',
'fontSize'
])
},
watch: {
// Auto-save settings when they change
theme() { this.saveSettings() },
language() { this.saveSettings() },
notifications() { this.saveSettings() },
autoSave() { this.saveSettings() },
fontSize() { this.saveSettings() }
},
methods: {
saveSettings() {
const store = useSettingsStore()
store.saveToLocalStorage()
},
resetToDefaults() {
const store = useSettingsStore()
store.resetToDefaults()
}
}
}
Shopping Cart
js
import { mapWritableState } from 'pinia'
import { useCartStore } from '@/stores/cart'
export default {
computed: {
...mapWritableState(useCartStore, ['items']),
...mapState(useCartStore, [
'total',
'itemCount',
'shipping'
])
},
methods: {
updateQuantity(itemId, quantity) {
const item = this.items.find(item => item.id === itemId)
if (item) {
item.quantity = quantity
}
},
removeItem(itemId) {
const index = this.items.findIndex(item => item.id === itemId)
if (index > -1) {
this.items.splice(index, 1)
}
}
},
template: `
<div class="cart">
<div v-for="item in items" :key="item.id" class="cart-item">
<span>{{ item.name }}</span>
<input
v-model.number="item.quantity"
type="number"
min="1"
@change="updateQuantity(item.id, item.quantity)"
>
<button @click="removeItem(item.id)">Remove</button>
</div>
<div class="cart-summary">
<p>Items: {{ itemCount }}</p>
<p>Total: ${{ total }}</p>
</div>
</div>
`
}
Best Practices
1. Use for Form Fields and User Input
js
// ✅ Good - perfect for form fields
computed: {
...mapWritableState(useFormStore, [
'firstName',
'lastName',
'email',
'phone'
])
}
// ❌ Avoid - computed values should use getters
computed: {
...mapWritableState(useUserStore, [
'fullName' // This should be a getter, not writable state
])
}
2. Combine with Read-Only State
js
// ✅ Good - mix writable and read-only as needed
computed: {
// Writable state for user input
...mapWritableState(useFormStore, [
'name',
'email',
'message'
]),
// Read-only state for display
...mapState(useFormStore, [
'isValid',
'errors',
'submitting'
])
}
3. Validate on Change
js
computed: {
...mapWritableState(useFormStore, ['email'])
},
watch: {
email: {
handler(newEmail) {
const store = useFormStore()
store.validateField('email', newEmail)
},
immediate: true
}
}
4. Handle Nested State Carefully
js
// ✅ Good - use actions for complex nested updates
methods: {
updateUserProfile(profile) {
const store = useUserStore()
store.updateProfile(profile) // Use store action
}
}
// ❌ Avoid - complex nested mutations in component
computed: {
userProfile: {
get() {
return this.userStore.user.profile
},
set(value) {
// This can be error-prone and hard to track
this.userStore.user.profile = value
}
}
}
Migration from Vuex
Vuex Two-Way Computed
js
// Vuex
computed: {
message: {
get() {
return this.$store.state.message
},
set(value) {
this.$store.commit('updateMessage', value)
}
}
}
// Pinia with mapWritableState
computed: {
...mapWritableState(useMessageStore, ['message'])
}
Vuex v-model with Store
js
// Vuex - complex v-model setup
computed: {
inputValue: {
get() {
return this.$store.state.form.inputValue
},
set(value) {
this.$store.dispatch('updateInputValue', value)
}
}
}
// Pinia - simple v-model
computed: {
...mapWritableState(useFormStore, ['inputValue'])
}
// Template remains the same
// <input v-model="inputValue">
See Also
- mapState() - Map read-only store state
- mapStores() - Map entire stores
- mapActions() - Map store actions
- Store Instance - Store instance API
- Options API Guide - Using Pinia with Options API