子组件更新Inertia useForm时的响应性异常问题排查
响应性异常排查:FilterMultiSelector更新Inertia useForm时显示错误选中项
问题描述
应用中使用多个FilterMultiSelector组件,子组件向父组件触发变更时更新Inertia的useForm,出现响应性异常:
- 添加Foo、Bar、Lorem三个资产后,移除Lorem,显示Foo和Bar(正常)
- 再移除Bar,却显示Foo和Lorem(异常)
- 移除Foo则显示Bar和Lorem(异常)
父组件代码
<script setup> import {useForm, usePage} from '@inertiajs/vue3' import FilterMultiSelector from '@/Composables/FilterMultiSelector.vue' const page = usePage() defineProps({ assets: { type: Array, }, }) const form = useForm({ name: '', assets: [], }) const submit = () => { form.post(route('store'), { onFinish: () => { form.name = '' form.assets = [] }, }) } const handleSelectedAssetsUpdate = (objects) => { console.log(objects) form.assets = [...objects] } </script> <template> <div class="my-2"> <FilterMultiSelector :objects="assets" name="Assets" :filterAttributes="['label', 'property_name']" @update:selectedObjects="handleSelectedAssetsUpdate" :selected-objects="form.assets"> <template #displayFilter="{object}"> <div class="flex justify-between text-xs"> <span class="font-bold">{{ object.label }}</span> <span class="">{{ object.property_name }}</span> </div> </template> <template #displaySelection="{object}"> <div class="flex justify-between gap-2"> <span class="font-bold">{{ object.label }}</span> <span>{{ object.property_name }}</span> </div> </template> </FilterMultiSelector> </div> </template>
FilterMultiSelector子组件代码
<script setup> import {computed, ref} from 'vue' import useDetectOutsideClick from '@/Composables/useDetectOutsideClick.js' import InputLabel from '@/Components/InputLabel.vue' import TextInput from '@/Components/TextInput.vue' import {XMarkIcon} from '@heroicons/vue/24/outline' let {name, objects, filterAttributes, selectedObjects} = defineProps({ name: { type: String, }, objects: { type: Array, }, filterAttributes: { type: Array, default: () => [], }, selectedObjects: { type: Array, default: () => [], }, }) const emit = defineEmits(['update:selectedObjects']) const componentRef = ref() const inputRef = ref() const searchString = ref('') const filteredObjects = computed(() => { return objects.filter((object) => { const searchLower = searchString.value.toLowerCase() return filterAttributes.length > 0 ? filterAttributes.some((key) => { const value = object[key] return typeof value === 'string' && value.toLowerCase().includes(searchLower) }) && !selectedObjects.includes(object) : Object.keys(object).some((key) => { const value = object[key] return typeof value === 'string' && value.toLowerCase().includes(searchLower) }) && !selectedObjects.includes(object) }) }) const add = (object) => { if (!selectedObjects.includes(object)) { selectedObjects.push(object) emit('update:selectedObjects', selectedObjects) } searchString.value = '' inputRef.value.focus() } const remove = (object) => { const newSelectedObjects = selectedObjects.filter((t) => t.id !== object.id) emit('update:selectedObjects', [...newSelectedObjects]) } useDetectOutsideClick(componentRef, () => { searchString.value = '' }) </script> <template> <div class="relative"> <InputLabel for="object" :value="name" /> <TextInput ref="inputRef" id="object" type="text" class="mt-1 block w-full" v-model="searchString" placeholder="Search object ..." auotofocus /> <div v-if="filteredObjects.length && searchString" class="absolute left-[.5%] top-[60px] z-10 w-[99%] rounded border bg-white p-4 shadow" ref="componentRef"> <div v-for="object in filteredObjects.slice(0, 10)" :key="object.id" @click="add(object)" class="rounded px-2 py-1 text-sm hover:cursor-pointer hover:bg-gray-100"> <slot name="displayFilter" :object="object" /> </div> <div v-if="filteredObjects.length > 10" class="pl-2 text-xs tracking-widest"> ... </div> </div> <div v-if="selectedObjects.length" class="mt-2 flex flex-wrap gap-2"> <div v-for="object in selectedObjects" :key="object.id" class="inline flex max-w-full items-center whitespace-nowrap rounded bg-primary-500 px-2 py-1 text-xs text-white hover:cursor-pointer hover:bg-primary-400" @click="remove(object)"> <slot name="displaySelection" :object="object" /> <XMarkIcon class="ml-1 h-4 w-4" /> </div> </div> </div> </template>
assets数据结构
assets: Array 0: Reactive id: 1 label:"Foo" property_name:"Bar" 1 ...
原因分析
核心问题在于子组件直接修改父组件传递的props数组,结合Inertia useForm的响应性机制,导致引用混乱:
- 破坏单向数据流:子组件
add方法中直接调用selectedObjects.push(object),修改了父组件form.assets的原数组引用(props为引用类型),导致父子组件共享同一数组实例。 - Inertia响应性追踪失效:useForm的表单对象是封装后的响应式对象,直接修改数组内部元素(如push)无法被其响应系统正确追踪,导致视图更新异常。
- 数组引用不一致:移除操作创建了新数组,但添加操作仍在旧引用上修改,导致组件间数组状态始终混乱,视图渲染基于错误的数组实例。
解决方案
1. 子组件中避免直接修改props,始终传递新数组
修改子组件add方法,创建新数组后再emit给父组件:
const add = (object) => { if (!selectedObjects.includes(object)) { // 创建新数组,保留原props引用的独立性 const newSelected = [...selectedObjects, object] emit('update:selectedObjects', newSelected) } searchString.value = '' inputRef.value.focus() }
2. 子组件使用本地状态维护选中项(可选优化)
通过本地ref隔离props,避免直接依赖父组件传递的数组,同时监听props变化同步状态:
<script setup> // 替换原selectedObjects的直接使用 import {computed, ref, watch} from 'vue' const localSelected = ref([...selectedObjects]) // 监听父组件props变化,同步本地状态 watch(() => selectedObjects, (newVal) => { localSelected.value = [...newVal] }, { deep: true }) // 修改add和remove方法,基于localSelected操作 const add = (object) => { if (!localSelected.value.includes(object)) { localSelected.value = [...localSelected.value, object] emit('update:selectedObjects', localSelected.value) } searchString.value = '' inputRef.value.focus() } const remove = (object) => { localSelected.value = localSelected.value.filter(t => t.id !== object.id) emit('update:selectedObjects', localSelected.value) } // 修改filteredObjects计算属性,使用localSelected const filteredObjects = computed(() => { return objects.filter((object) => { const searchLower = searchString.value.toLowerCase() return filterAttributes.length > 0 ? filterAttributes.some((key) => { const value = object[key] return typeof value === 'string' && value.toLowerCase().includes(searchLower) }) && !localSelected.value.includes(object) : Object.keys(object).some((key) => { const value = object[key] return typeof value === 'string' && value.toLowerCase().includes(searchLower) }) && !localSelected.value.includes(object) }) }) </script>
3. 验证父组件数组更新的引用唯一性
在父组件handleSelectedAssetsUpdate中可添加日志,确认每次接收的都是新数组:
const handleSelectedAssetsUpdate = (objects) => { console.log('新数组与原数组是否同一引用:', objects === form.assets) form.assets = [...objects] }
内容的提问来源于stack exchange,提问作者DirkReuter
相关产品推荐
相关产品推荐

