You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用useIntersectionObserver实现无限滚动后提交表单触发replaceState错误

问题描述

我使用useIntersectionObserver实现无限滚动功能,滚动时一切正常,但滚动后提交帖子表单时,出现以下错误:

错误信息

DOMException: Failed to execute 'replaceState' on 'History'


相关代码

web.php

Route::get('/dashboard', [DashboardController::class, 'index'])->middleware(['auth', 'verified'])->name('dashboard');

Dashboard.vue

<template>
    <Head title="Dashboard" />

    <AuthenticatedLayout>
        <div class="py-2">
            <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
                <div class="overflow-hidden">
                    <div class="py-6 text-gray-900 flex">
                        <section class="basis-2/5">
                            <h3>Profile</h3>
                            <h3>Logout</h3>
                        </section>
                        <section class="basis-3/5 px-3">
                            <Form />
                            
                            <div v-if="posts.data.length" class="mt-12" id="posts">
                                <Post
                                    v-for="post in posts.data"
                                    :key="post.id"
                                    :post="post"
                                />

                                <div ref="bottom" class="-translate-y-32"></div>
                            </div>
                        </section>
                    </div>
                </div>
            </div>
        </div>
    </AuthenticatedLayout>
</template>

<script setup>

import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head } from '@inertiajs/vue3';
import Form from '@/Components/Post/Form.vue';
import Post from '@/Components/Post.vue';
import { onMounted, ref } from 'vue';
import { useIntersectionObserver } from '@vueuse/core';
import axios from 'axios';

const props = defineProps({
    posts: Object
});

const bottom = ref(null);

const { stop } = useIntersectionObserver(bottom, ([{ isIntersecting }]) => {
    if (!isIntersecting) {
        return;
    }

    axios.get(`${props.posts.meta.path}?cursor=${props.posts.meta.next_cursor}`)
        .then((response) => {
            props.posts.data = [...props.posts.data, ...response.data.data];
            props.posts.meta = response.data.meta;

            if (!response.data.meta.next_cursor) {
                stop();
            }
        });
});

onMounted(() => {
    document.addEventListener('click', function (event) {
        if (event.target.matches('.post-body .mention')) {
            if (parseInt(event.target.dataset.id) > 0) {
                alert(event.target.outerText);
            }
        }
    });
});
</script>

Form.vue

<template>
    <div>
        <form @submit.prevent="post">
            <div>
                <Tiptap v-model="form.body" placeholder="Got something to share?" />

                <!-- <TextArea
                    id="body"
                    type="text"
                    class="mt-4 block w-full"
                    rows="3"
                    placeholder="Got something to share?"
                    v-model="form.body"
                    required
                /> -->
                                    
                <InputError class="mt-2" :message="form.errors.body" />
            </div>
            <div class="mt-3">
                <PrimaryButton :disabled="form.processing || form.body.length < 1">Post</PrimaryButton>
            </div>
        </form>
    </div>
</template>

<script setup>
import TextArea from '@/Components/TextArea.vue';
import InputError from '@/Components/InputError.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import { useForm } from '@inertiajs/vue3';
import Tiptap from '../Tiptap.vue';

const form = useForm('PostForm', {
    body: ''
});

const post = () => {
    form.post(route('posts.store'), {
        preserveScroll: true,
        preserveState: false,
        onSuccess: (page) => {
            form.reset();

            console.log(page.props)
            //page.props.posts.data = [[], ...page.props.posts.data];
        },
        onFinish: (visit) => {
            console.log(visit)
        }
    });
};
</script>

解决方案

错误原因

直接修改Inertia传递的props.posts(包括data和meta字段)破坏了Inertia的单向数据流规则。Inertia期望props是不可变的,直接修改会干扰其内部状态管理,导致表单提交后执行history.replaceState时出错。

修复步骤

1. 在Dashboard.vue中用本地ref维护帖子数据

不要直接操作props,复制初始数据到本地响应式变量中:

// 替换原props定义后的代码
const props = defineProps({ posts: Object });
// 初始化本地状态,复制props数据
const localPosts = ref({
  data: [...props.posts.data],
  meta: { ...props.posts.meta }
});

const bottom = ref(null);

const { stop } = useIntersectionObserver(bottom, ([{ isIntersecting }]) => {
    if (!isIntersecting) {
        return;
    }

    axios.get(`${localPosts.value.meta.path}?cursor=${localPosts.value.meta.next_cursor}`)
        .then((response) => {
            // 修改本地状态而非props
            localPosts.value.data = [...localPosts.value.data, ...response.data.data];
            localPosts.value.meta = response.data.meta;

            if (!response.data.meta.next_cursor) {
                stop();
            }
        });
});

同时更新模板中的循环源:

<div v-if="localPosts.data.length" class="mt-12" id="posts">
    <Post
        v-for="post in localPosts.data"
        :key="post.id"
        :post="post"
    />

    <div ref="bottom" class="-translate-y-32"></div>
</div>

2. 表单提交后同步新帖子到本地列表

在Form.vue中添加emit,通知父组件新增的帖子:

// Form.vue的script部分添加emit定义
const emit = defineEmits(['postCreated']);

// 修改onSuccess回调
onSuccess: (page) => {
    form.reset();
    // 假设新创建的帖子在返回数据的第一个位置
    emit('postCreated', page.props.posts.data[0]);
}

在Dashboard.vue中监听这个事件并更新本地列表:

<!-- 修改Form组件的引用 -->
<Form @post-created="addNewPost" />
// 在Dashboard.vue的script中添加方法
const addNewPost = (newPost) => {
    // 将新帖子插入列表顶部
    localPosts.value.data.unshift(newPost);
};

内容的提问来源于stack exchange,提问作者Blues Clues

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 22:42:07