Next.js客户端组件传递服务端Action返回的数组Props时触发无限循环导致栈溢出问题排查
问题背景
我太懂你遇到的这个坑了!你在Next.js的服务端组件(page.tsx)里用服务端Action从数据库拉取了订单坐标数组,传给客户端组件MapSidebar时直接爆了栈溢出,但换成静态数组或者把服务端返回的数组转成JSON字符串就完全正常——这种情况我之前帮朋友排查过,核心问题出在服务端Action返回的数据不是纯原生JavaScript对象。
为什么会出现这个问题?
你从数据库查出来的结果,大概率是ORM(比如Prisma、Mongoose这类工具)返回的实例对象,不是普通的JS对象。这些实例会带有ORM内部的代理、元数据(比如__typename、不可枚举的隐藏属性),甚至可能存在隐式的循环引用(比如关联模型的反向引用,虽然你当前展示的data里没体现,但序列化过程中可能触发)。
当Next.js把服务端组件的Props传给客户端组件时,会执行hydration(水合)过程:把服务端序列化的数据转成客户端能识别的JS对象。如果碰到带服务端特殊标识的对象,React处理Props的逻辑会递归遍历对象的所有属性,一旦触发循环引用或者无限递归的遍历路径,就会直接抛出Maximum call stack size exceeded错误。
而静态数组是你手工写的纯原生JS对象,没有任何额外元数据,Next.js能正常处理;用JSON.stringify(location)再转成对象时,相当于把ORM实例彻底转成了纯JSON结构,去掉了所有服务端附加的特殊属性,自然就不会触发递归溢出了。
怎么解决?
最推荐在服务端Action里提前处理数据,确保返回给组件的是纯原生JS对象,有两种常用且靠谱的方法:
方法1:用JSON序列化/反序列化清洗数据
简单粗暴,直接把查询结果转成纯JS对象,彻底剥离ORM的特殊属性:
export async function getAssignedOrderCoordinates() { // 你的数据库查询逻辑 const dbOrders = await db.order.findMany({ // 你的查询条件、字段选择 }); // 清洗掉ORM实例的特殊属性,转成纯JS对象数组 return JSON.parse(JSON.stringify(dbOrders)); }
方法2:手动映射需要的字段
如果觉得JSON转来转去不够优雅,也可以手动映射每个字段,只保留你业务需要的数据,完全规避ORM实例的影响:
export async function getAssignedOrderCoordinates() { const dbOrders = await db.order.findMany(...); return dbOrders.map(order => ({ order_id: order.order_id, city: order.city, address: { block: order.address.block, street: order.address.street, house: order.address.house, jaddah: order.address.jaddah }, asignee_name: order.asignee_name, asignee: order.asignee, coordinates: order.coordinates })); }
额外提示
如果服务端处理后还是有问题,可以检查下MapSidebar组件里有没有对Props做递归遍历的逻辑(比如深拷贝、递归渲染),但这种情况比较少见,优先处理服务端的数据是更高效的方案。
总结
本质就是服务端返回的ORM实例带有额外元数据,导致Next.js的hydration过程触发无限递归。只要把数据转成纯原生JS对象,不管是用JSON清洗还是手动映射,都能解决这个栈溢出问题。
内容来源于stack exchange

