Skip to content

mapWritableState()

将 store 状态属性映射为 Options API 组件中的可写计算属性。与 mapState() 不同,这允许从组件直接修改状态属性。

函数签名

ts
function mapWritableState<T>(
  useStore: () => T,
  keys: (keyof T)[] | Record<string, keyof T>
): WritableComputedOptions

参数

  • useStore: Store 定义函数
  • keys: 状态属性名称数组或自定义映射对象

返回值

用于 Options API 组件的可写计算属性对象。

基本用法

数组语法

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

export default {
  computed: {
    // 将 this.count, this.name 映射为可写计算属性
    ...mapWritableState(useCounterStore, ['count', 'name'])
  },
  
  methods: {
    increment() {
      this.count++ // 直接修改
    },
    
    updateName(newName) {
      this.name = newName // 直接赋值
    }
  },
  
  template: `
    <div>
      <p>计数: {{ count }}</p>
      <button @click="increment">+</button>
      
      <input v-model="name" placeholder="输入名称">
    </div>
  `
}

对象语法

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

export default {
  computed: {
    // 自定义属性名称
    ...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="姓名">
      <input v-model="userEmail" placeholder="邮箱">
      <input v-model.number="userAge" placeholder="年龄">
      <button type="submit">保存</button>
    </form>
  `
}

表单绑定

双向数据绑定

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>名:</label>
        <input v-model="firstName" type="text">
      </div>
      
      <div class="form-group">
        <label>姓:</label>
        <input v-model="lastName" type="text">
      </div>
      
      <div class="form-group">
        <label>邮箱:</label>
        <input v-model="email" type="email">
      </div>
      
      <div class="form-group">
        <label>电话:</label>
        <input v-model="phone" type="tel">
      </div>
      
      <div class="form-group">
        <label>地址:</label>
        <textarea v-model="address"></textarea>
      </div>
    </form>
  `
}

复杂表单状态

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">
          深色模式
        </label>
      </div>
      
      <div class="setting-item">
        <label>语言:</label>
        <select v-model="language">
          <option value="zh">中文</option>
          <option value="en">English</option>
          <option value="ja">日本語</option>
        </select>
      </div>
      
      <div class="setting-item">
        <label>
          <input v-model="notifications" type="checkbox">
          启用通知
        </label>
      </div>
      
      <div class="setting-item">
        <label>
          <input v-model="autoSave" type="checkbox">
          自动保存
        </label>
      </div>
    </div>
  `
}

高级用法

嵌套状态属性

js
import { mapWritableState } from 'pinia'
import { useProfileStore } from '@/stores/profile'

// Store 定义
export const useProfileStore = defineStore('profile', {
  state: () => ({
    user: {
      personal: {
        name: '',
        email: '',
        avatar: ''
      },
      preferences: {
        theme: 'light',
        language: 'zh',
        notifications: true
      }
    }
  })
})

// 组件
export default {
  computed: {
    // 注意: mapWritableState 最适合扁平状态属性
    // 对于嵌套属性,考虑使用 store action 或直接 store 访问
    ...mapWritableState(useProfileStore, {
      // 这些需要在 store 中扁平化或以不同方式访问
      userName: (store) => store.user.personal.name,
      userEmail: (store) => store.user.personal.email
    })
  },
  
  methods: {
    // 嵌套状态的更好方法
    updatePersonalInfo(info) {
      const store = useProfileStore()
      store.user.personal = { ...store.user.personal, ...info }
    }
  }
}

条件可写状态

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

export default {
  computed: {
    ...mapWritableState(useUserStore, ['name', 'email']),
    
    // 基于用户权限的条件可写
    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
        }
      }
    }
  }
}

数组和对象状态

js
import { mapWritableState } from 'pinia'
import { useListStore } from '@/stores/list'

export default {
  computed: {
    ...mapWritableState(useListStore, ['items', 'filters'])
  },
  
  methods: {
    addItem(item) {
      this.items.push(item) // 直接数组修改
    },
    
    removeItem(index) {
      this.items.splice(index, 1) // 直接数组修改
    },
    
    updateFilter(key, value) {
      this.filters[key] = value // 直接对象修改
    },
    
    clearFilters() {
      this.filters = {} // 直接对象替换
    }
  },
  
  template: `
    <div>
      <div class="filters">
        <input 
          v-model="filters.search" 
          placeholder="搜索..."
        >
        <select v-model="filters.category">
          <option value="">所有分类</option>
          <option value="tech">技术</option>
          <option value="design">设计</option>
        </select>
      </div>
      
      <ul class="items">
        <li v-for="(item, index) in items" :key="item.id">
          {{ item.name }}
          <button @click="removeItem(index)">删除</button>
        </li>
      </ul>
      
      <button @click="addItem({ id: Date.now(), name: '新项目' })">
        添加项目
      </button>
    </div>
  `
}

TypeScript

类型安全

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>
})

泛型可写状态映射

ts
function createWritableStateMapper<
  T extends Record<string, any>,
  K extends keyof T
>(
  useStore: () => T,
  keys: K[]
): WritableComputedOptions<Pick<T, K>> {
  return mapWritableState(useStore, keys)
}

// 使用
const writableUserState = createWritableStateMapper(
  useUserStore, 
  ['name', 'email', 'age']
)

export default {
  computed: {
    ...writableUserState
  }
}

与其他映射函数的比较

vs mapState

js
// mapState - 只读访问
computed: {
  ...mapState(useCounterStore, ['count']),
  
  // 这不会工作 - 计算属性默认是只读的
  // this.count = 10 // 错误!
}

// mapWritableState - 读写访问
computed: {
  ...mapWritableState(useCounterStore, ['count']),
  
  // 这可以工作 - 可写计算属性
  // this.count = 10 // ✅ 可以!
}

vs 直接 Store 访问

js
// 直接 store 访问
export default {
  computed: {
    count: {
      get() {
        const store = useCounterStore()
        return store.count
      },
      set(value) {
        const store = useCounterStore()
        store.count = value
      }
    }
  }
}

// mapWritableState - 等价但更简洁
export default {
  computed: {
    ...mapWritableState(useCounterStore, ['count'])
  }
}

性能考虑

选择性映射

js
// ✅ 好 - 只映射需要修改的内容
computed: {
  ...mapWritableState(useFormStore, [
    'firstName', // 将被修改
    'lastName',  // 将被修改
    'email'      // 将被修改
  ]),
  
  // 对只读属性使用 mapState
  ...mapState(useFormStore, [
    'isValid',   // 只读
    'errors',    // 只读
    'submitting' // 只读
  ])
}

// ❌ 效率较低 - 将所有内容映射为可写
computed: {
  ...mapWritableState(useFormStore, [
    'firstName', 'lastName', 'email',
    'isValid', 'errors', 'submitting' // 这些不需要可写
  ])
}

批量更新

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

export default {
  computed: {
    ...mapWritableState(useFormStore, [
      'firstName',
      'lastName',
      'email'
    ])
  },
  
  methods: {
    // ✅ 好 - 批量更新
    updateProfile(profile) {
      // 使用 store action 进行批量更新
      const store = useFormStore()
      store.updateProfile(profile)
    },
    
    // ❌ 效率较低 - 单独更新
    updateProfileIndividually(profile) {
      this.firstName = profile.firstName // 触发响应式
      this.lastName = profile.lastName   // 触发响应式
      this.email = profile.email         // 触发响应式
    }
  }
}

常见模式

表单验证

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) {
      // 邮箱变化时验证
      const store = useFormStore()
      store.validateEmail(newEmail)
    },
    
    password(newPassword) {
      // 密码变化时验证
      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">提交</button>
    </form>
  `
}

设置面板

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

export default {
  computed: {
    ...mapWritableState(useSettingsStore, [
      'theme',
      'language',
      'notifications',
      'autoSave',
      'fontSize'
    ])
  },
  
  watch: {
    // 设置变化时自动保存
    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()
    }
  }
}

购物车

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)">删除</button>
      </div>
      
      <div class="cart-summary">
        <p>商品数量: {{ itemCount }}</p>
        <p>总计: ¥{{ total }}</p>
      </div>
    </div>
  `
}

最佳实践

1. 用于表单字段和用户输入

js
// ✅ 好 - 非常适合表单字段
computed: {
  ...mapWritableState(useFormStore, [
    'firstName',
    'lastName',
    'email',
    'phone'
  ])
}

// ❌ 避免 - 计算值应该使用 getter
computed: {
  ...mapWritableState(useUserStore, [
    'fullName' // 这应该是 getter,而不是可写状态
  ])
}

2. 与只读状态结合

js
// ✅ 好 - 根据需要混合可写和只读
computed: {
  // 用户输入的可写状态
  ...mapWritableState(useFormStore, [
    'name',
    'email',
    'message'
  ]),
  
  // 显示的只读状态
  ...mapState(useFormStore, [
    'isValid',
    'errors',
    'submitting'
  ])
}

3. 变化时验证

js
computed: {
  ...mapWritableState(useFormStore, ['email'])
},

watch: {
  email: {
    handler(newEmail) {
      const store = useFormStore()
      store.validateField('email', newEmail)
    },
    immediate: true
  }
}

4. 小心处理嵌套状态

js
// ✅ 好 - 对复杂嵌套更新使用 action
methods: {
  updateUserProfile(profile) {
    const store = useUserStore()
    store.updateProfile(profile) // 使用 store action
  }
}

// ❌ 避免 - 组件中的复杂嵌套修改
computed: {
  userProfile: {
    get() {
      return this.userStore.user.profile
    },
    set(value) {
      // 这可能容易出错且难以跟踪
      this.userStore.user.profile = value
    }
  }
}

从 Vuex 迁移

Vuex 双向计算属性

js
// Vuex
computed: {
  message: {
    get() {
      return this.$store.state.message
    },
    set(value) {
      this.$store.commit('updateMessage', value)
    }
  }
}

// Pinia 使用 mapWritableState
computed: {
  ...mapWritableState(useMessageStore, ['message'])
}

Vuex v-model 与 Store

js
// Vuex - 复杂的 v-model 设置
computed: {
  inputValue: {
    get() {
      return this.$store.state.form.inputValue
    },
    set(value) {
      this.$store.dispatch('updateInputValue', value)
    }
  }
}

// Pinia - 简单的 v-model
computed: {
  ...mapWritableState(useFormStore, ['inputValue'])
}

// 模板保持不变
// <input v-model="inputValue">

相关链接

Released under the MIT License.