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

使用NextJS、Supabase和React Query时出现调用栈溢出错误

NextJS跳转动态路由时栈溢出错误排查

错误信息

✓ Compiled in 1316ms (829 modules)
 ○ Compiling /lobby/[id] ...
 ✓ Compiled /lobby/[id] in 2s (1728 modules)
 ⨯ next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.edge.development.js (1469:16) @ logRecoverableError
 ⨯ RangeError: Maximum call stack size exceeded
    at stringify (<anonymous>)
    at stringify (<anonymous>)
    at stringify (<anonymous>)
    at stringify (<anonymous>)
    at stringify (<anonymous>)
    at stringify (<anonymous>)
digest: "2761827113"
  1467 |         request.pendingChunks++;
  1468 |         prevKeyPath = request.nextChunkId++;
> 1469 |         task = logRecoverableError(request, key, task);
       |                ^
  1470 |         emitErrorChunk(request, prevKeyPath, task, key);
  1471 |         return parent
  1472 |           ? "$L" + prevKeyPath.toString(16)

相关代码

useFetch.ts

import { createClient } from "@/utils/supabase/client";
import { useMutation } from "@tanstack/react-query";

const supabase = createClient();

export const useFetchTeamLogin = () => {
   return useMutation({
      mutationFn: async (teamName: string) => {
  try {
    const { data, error } = await supabase
      .from("teams")
      .select("teamToken")
      .eq("teamName", teamName)
      .single();

    if (error) throw new Error(error.message);
    return data;
  } catch (err) {
    console.error("Mutation error:", err);
    throw err;
  }
},
});
};

login.tsx

"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Dialog,
  DialogTrigger,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
} from "@/components/ui/dialog";
import { useFetchTeamLogin } from "@/hooks/useFetch";

export default function Login() {
  const [teamName, setTeamName] = useState<string>("");
  const { mutate: fetchTeamLogin, error, isError, isPending } = useFetchTeamLogin();
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!teamName.trim()) {
      console.error("Team name cannot be empty.");
      return;
    }

    fetchTeamLogin(teamName.toLowerCase(), {
      onSuccess: (data) => {
        if (data?.teamToken) {
          router.push(`/lobby/${data.teamToken}`);
        } else {
          console.error("Team token is undefined.");
        }
      },
      onError: (error) => {
        console.error("Error during login:", error.message);
      },
    });
  };

  return (
    <div className="relative">
      <Dialog>
        <DialogTrigger asChild>
          <Button className="w-80 font-bold p-5 text-white bg-orange-500 hover:bg-orange-700 cursor-pointer transition-colors">
            Join the Hack!
          </Button>
        </DialogTrigger>
        <DialogContent className="border-none bg-[#1e1e1e] w-[350px] rounded-md">
          <DialogHeader>
            <DialogTitle className="text-2xl text-orange-600 font-pixelify text-left">
              {isError ? <p className="text-red-500">{error?.message}</p> : "Login"}
            </DialogTitle>
            <DialogDescription className="text-md text-gray-600 text-left">
              Please enter your correct information
            </DialogDescription>
          </DialogHeader>
          <form onSubmit={handleSubmit}>
            <Input
              type="text"
              placeholder="Enter Team Name"
              className="w-full text-gray-400 p-3 bg-transparent border border-gray-300 rounded-md mb-2"
              value={teamName}
              onChange={(e) => setTeamName(e.target.value)}
              required
            />
            <p className="text-[10px] text-gray-300 text-right mb-3">
              Input in lowercase with no whitespace character
            </p>
            <Button
              type="submit"
              disabled={isPending}
              size="lg"
              className="w-full bg-orange-500 text-white hover:bg-orange-600 transition-colors"
            >
              {isPending ? "Authenticating..." : "Login"}
            </Button>
          </form>
        </DialogContent>
      </Dialog>
    </div>
  );
}

lobby.tsx

export default function Lobby() {
  return (
    <div className="flex items-center justify-center h-screen">
      <h1 className="text-lg font-bold mb-4">
        Welcome to the Lobby
      </h1>
    </div>
  );
}

问题原因及解决方案

1. Supabase返回数据存在循环引用

Supabase查询返回的data对象并非纯JSON结构,内部可能带有原型链属性或循环引用的元数据。当React Query缓存该对象,或组件尝试序列化它时,会触发JSON.stringify栈溢出。

解决: 修改useFetch.ts,只返回需要的纯数据字段:

// ...
if (error) throw new Error(error.message);
// 只提取需要的teamToken,避免返回带有内部引用的Supabase响应对象
return { teamToken: data.teamToken };
// ...

2. 动态路由文件结构错误

Next.js的动态路由要求文件命名为[param].tsx,且放在对应路由层级的文件夹下。比如访问/lobby/[teamToken],需要创建app/lobby/[teamToken]/page.tsx文件,而非根目录下的lobby.tsx。文件结构不匹配会导致路由匹配异常,触发无限渲染或序列化错误。

解决: 调整文件结构:

  • 创建app/lobby文件夹
  • 在其中新建[teamToken]/page.tsx,将原lobby.tsx的内容移入该文件

3. React Query错误对象的序列化问题

error对象可能带有循环引用(比如包含Supabase的原始错误对象),在客户端组件中渲染时,若父组件为服务端组件,可能触发序列化错误。

优化: 在useFetch.ts中捕获错误时,只抛出包含信息的纯Error对象,避免传递原始的Supabase错误:

// ...
if (error) {
  // 仅提取错误信息,避免传递带有循环引用的原始错误对象
  throw new Error(error.message);
}
// ...

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 10:14:56