更新页面位置时axios+MongoDB的findOneAndReplace正常,新增字段后返回undefined
我是React新手,麻烦各位帮我看下代码哪里有问题,感谢!
本应用可在黑板上创建新卡片(即“pages”),当页面在黑板上任意位置拖拽时,会调用MongoDB的findOneAndReplace接口,用带新坐标的页面状态替换原有页面,从而实现页面位置在应用关闭后仍可持久化存储。除此之外,用户可将组件(widgets)拖拽到每个页面上,这些组件也会以对象数组的形式保存在页面卡片的状态和数据库中。
我花了好几个晚上都没定位到这个问题:拖拽、新增、删除页面时,状态更新正常,数据库调用也正常;拖拽一个或多个widget到页面上时,新增widget对象数组后的状态更新正常,应用中也能正常展示,但对应的POST请求返回undefined。更奇怪的是,出现该问题后再拖拽这个页面,接口也开始返回undefined,页面行为也出现异常,而没有添加widget的页面一切正常。
我尝试获取更多错误信息,发现只要page对象中添加了widget字段,POST方法似乎完全没有被调用。我在POST方法中加了console日志,添加widget后就算还没执行findOneAndReplace调用,日志也不会打印。
以下是成功传入POST方法的page对象结构(未拖拽widget到页面时):
以下是添加多个widget后的page对象结构,此时使用和移动页面相同的POST方法更新页面会返回undefined:
更反常的是,拖拽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

