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

更新页面位置时axios+MongoDB的findOneAndReplace正常,新增字段后返回undefined

React拖拽卡片持久化存储异常问题

我是React新手,麻烦各位帮我看下代码哪里有问题,感谢!

本应用可在黑板上创建新卡片(即“pages”),当页面在黑板上任意位置拖拽时,会调用MongoDB的findOneAndReplace接口,用带新坐标的页面状态替换原有页面,从而实现页面位置在应用关闭后仍可持久化存储。除此之外,用户可将组件(widgets)拖拽到每个页面上,这些组件也会以对象数组的形式保存在页面卡片的状态和数据库中。

我花了好几个晚上都没定位到这个问题:拖拽、新增、删除页面时,状态更新正常,数据库调用也正常;拖拽一个或多个widget到页面上时,新增widget对象数组后的状态更新正常,应用中也能正常展示,但对应的POST请求返回undefined。更奇怪的是,出现该问题后再拖拽这个页面,接口也开始返回undefined,页面行为也出现异常,而没有添加widget的页面一切正常。

我尝试获取更多错误信息,发现只要page对象中添加了widget字段,POST方法似乎完全没有被调用。我在POST方法中加了console日志,添加widget后就算还没执行findOneAndReplace调用,日志也不会打印。

以下是成功传入POST方法的page对象结构(未拖拽widget到页面时):
传入findOneAndReplace的Page对象

以下是添加多个widget后的page对象结构,此时使用和移动页面相同的POST方法更新页面会返回undefined:
添加widget后POST方法调用失败

更反常的是,拖拽widget到页面上后,页面的x、y坐标还会发生轻微偏移...

我不确定是页面组件内部的widget状态更新和“pagetree”黑板上的页面状态存在冲突,还是我Axios的用法有问题?如果需要我可以提供完整代码,麻烦大家帮忙看看,谢谢!

pageCard组件(即黑板上的卡片组件)的Widget回调方法和onDragEnd方法

const updateWidgets = useCallback((newWidgets) => {
    setWidgets(newWidgets);
      }, [widgets]);

function handleOnDragEnd(result) {
    if (!result.destination) return;
    const items = Array.from(widgets);
    const [reorderedItem] = items.splice(result.source.index, 1);
    items.splice(result.destination.index, 0, reorderedItem);
    setWidgets(items);
    widgetCallback(items,_id);
  }

调用API的父组件pageTree(页面黑板)代码

import React, { useCallback, useEffect, useState } from 'react';
import PageCard from './PageCard';
import { useDrop } from "react-dnd";
import { ItemTypes } from "../Utils/items";
import Api from "../api.js"

const styles = {
  width: 1000,
  height: 3000,
  border: '1px solid black',
  position: 'relative',
};

function PageTree({AddNewPageFunc}) {
  const [pages, setPages] = useState([]);
  const updatePage = useCallback((droppedPage) => {
  const updatedPages = pages.map(page => droppedPage._id == page._id ? droppedPage : page);
  //console.log("updated pages ",updatedPages);
  setPages(updatedPages);
  //console.log("set pages ",pages);
    }, [pages]);

  const [{isOver}, drop] = useDrop(() => ({
        accept: ItemTypes.PAGECARD,
        drop(page, monitor) {
            const delta = monitor.getDifferenceFromInitialOffset();
            let x = Math.round(page.x + delta.x);
            let y = Math.round(page.y + delta.y);
            page.x = x;
            page.y = y;
            page.widgets = [{name: "widget", thing: "widgetthing", header: "otherWidgetThing"}];
            savePage(page, page._id);
            updatePage(page);
            return undefined;
        },
    }), [updatePage]);

    const savePage = (pageDetails, id) => {
        console.log("function called to update page: ",pageDetails);
        Api.withToken().post('/pageupdate/'+id,
            pageDetails
        ).then(function (response) {
            console.log("?worked ",response.data)
        }).catch(function (error) {
          console.log("page save failed for some reason: ",error.response);
        });
    }

    React.useEffect(() => {
      AddNewPageFunc.current = AddNewPage
    }, [pages])

    const AddNewPage = useCallback(() => {
      Api.withToken().post('/addblankpage/'
        ).then(function (response) {
          console.log("produced: ",response.data);
          setPages([...pages,response.data])
      }).catch(function (error) {
        console.log(error.response);
      });
    }, [pages]);

    const handleDeletedCallback = useCallback((deletedIndex) => {
      //I HAVE NO IDEA WHY I HAVE TO CALL SETPAGES TWICE, BUT THAT IS THE RULES, IT WON'T WORK IF YOU DON'T...
      setPages(pages.splice(deletedIndex, 1));
      setPages(pages);
    }, [pages]);

    const WidgetAddCallback = (widgets, pageID) => {
      console.log("widget callback fired requesting widgets ",widgets," for ID ",pageID)
      const newPageIndex = pages.findIndex(page => page._id === pageID);
      const updatedPage = Object.assign(pages[newPageIndex], {widgets: widgets});
      console.log("updated page is ",updatedPage)
      savePage(updatedPage, pageID);
      updatePage(updatedPage);
    }

    useEffect(() => {
      Api.withToken().get('/pages/') //can add in a prop to return only a given tree once the app gets bigger
      .then(res => {
        setPages(res.data);
      })
    }, []);

  return (
      <div ref={drop} style={styles}>
          {pages.map((page, index) => (<PageCard page={page} id={page._id} key={page._id} index={index} widgetCallback ={WidgetAddCallback} deleteCallback={handleDeletedCallback} handleMaximise={() => handleMaximise(page)} handleCopy={() => handleCopy(page)}/>))}
      </div>
      )
}

export default PageTree;

Axios封装的API文件代码

import axios from 'axios';
const baseURL = process.env.REACT_APP_BASE_URL || "http://localhost:3001"

export default {
  noToken() {
    return axios.create({
      baseURL: baseURL
    });
  },
  
  withToken() {
    const tokenStr = window.sessionStorage.getItem("token")
    return axios.create({
      baseURL: baseURL,
      headers: {"Authorization" : `Bearer ${tokenStr}`}
    });
  } 
}

页面更新的POST接口代码,添加widget后该接口完全不会被调用

const express = require('express')
const router =new express.Router()
const Pages = require('../models/page')
const auth = require('../middleware/authentication')

router.post('/pageupdate/:_id',auth, async(req,res)=>{
    //console.log("received for pageUpdate API: ",req.body);
    const query = { "_id": req.params };
    const replacementPage = req.body;
    const options = { "returnNewDocument": false };
    Pages.findOneAndReplace(query, replacementPage, null, function (err, doc) {
        if (err){
            console.log(err)
        }
        else{
            console.log("replaced successfully");
        }})
})

修复方案

1. 后端接口逻辑错误修复

当前接口存在两个致命错误:一是查询参数拼接错误,req.params是路由参数集合对象,直接赋值给_id会导致查询条件不匹配;二是接口没有返回任何响应,导致前端请求一直挂起,返回undefined。
修复后的接口代码:

router.post('/pageupdate/:_id',auth, async(req,res)=>{
    const query = { "_id": req.params._id };
    const replacementPage = req.body;
    const options = { returnNewDocument: false };
    Pages.findOneAndReplace(query, replacementPage, options, function (err, doc) {
        if (err){
            console.log(err)
            return res.status(500).json({success: false, error: err})
        }
        res.json({success: true, data: doc})
    })
})

2. 补全Mongoose模型字段

检查../models/page文件的Page模型定义,添加widgets字段配置,否则mongoose默认的严格模式会自动过滤未定义的widgets字段,导致保存失败:

// 在Page模型的schema中添加以下配置
widgets: {
  type: Array,
  default: []
}

3. 修复前端状态原地修改问题

所有异常行为(坐标偏移、状态更新异常、需要两次调用setPages)的核心原因是直接原地修改了React的状态对象/数组,违反了React不可变更新的原则,修复如下:

修复删除回调

const handleDeletedCallback = useCallback((deletedIndex) => {
  const newPages = pages.filter((_, index) => index !== deletedIndex);
  setPages(newPages);
}, [pages]);

修复组件添加回调

const WidgetAddCallback = (widgets, pageID) => {
  const newPageIndex = pages.findIndex(page => page._id === pageID);
  const updatedPage = {...pages[newPageIndex], widgets: widgets};
  const newPages = [...pages];
  newPages[newPageIndex] = updatedPage;
  savePage(updatedPage, pageID);
  setPages(newPages);
}

修复页面拖拽逻辑

不要直接修改传入的page对象,也不要硬编码覆盖widgets字段:

drop(page, monitor) {
    const delta = monitor.getDifferenceFromInitialOffset();
    let x = Math.round(page.x + delta.x);
    let y = Math.round(page.y + delta.y);
    const updatedPage = {...page, x, y};
    savePage(updatedPage, page._id);
    const newPages = pages.map(item => item._id === page._id ? updatedPage : item);
    setPages(newPages);
    return undefined;
},

4. 冗余逻辑清理

移除冗余的updatePage方法,该方法依赖pages数组会触发闭包陷阱,导致拿到的状态不是最新值,所有状态更新直接用不可变方式调用setPages即可。


内容的提问来源于stack exchange,提问作者John Shanks

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.29 14:54:03