Skip to content

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

Released under the MIT License.