Next.js + Tanstack Query中useSuspenseQuery报data undefined错误
问题描述
我正在开发一个Next.js应用,已经实现了评论系统,支持用户给AI工具发布评论、回复评论以及回复他人的回复。其中<ReplyItemOperations>组件负责渲染点赞、点踩、回复按钮,内部用useSuspenseQuery获取用户对该回复的点赞/点踩状态。
组件里useSuspenseQuery的queryKey是["userReplyData", toolName, userEmail]。未登录状态下,<ToolPage>组件能正常渲染工具详情、评论和回复;但登录后页面直接无法渲染,报错:
Error: ["userReplyData","MakeAudio","user@gmail.com"] data is undefined
相关代码包括<ReplyItemOperations>组件逻辑,以及基于Supabase查询user_replies表的getUserReplyData函数。我怀疑错误和queryKey在登录时带有效userEmail、未登录时为undefined导致的缓存混乱有关,想知道具体原因和解决方法。
环境信息:
- Next.js 14.1.3
- Tanstack Query 5.40.1
- next-auth 4.24.5
原因分析
- 查询函数返回值问题:
getUserReplyData在用户登录后查询user_replies表时,可能没有找到匹配数据,直接返回了undefined,而useSuspenseQuery在Suspense模式下会将undefined视为错误抛出。 - QueryKey缓存冲突:未登录时
queryKey包含undefined(userEmail为空),TanStack Query会缓存这个无用户状态的查询结果;登录后queryKey变为带有效邮箱的新键,但如果查询函数返回空数据,Suspense模式下无法处理这种情况,直接触发页面崩溃。 - Suspense模式错误处理缺失:
useSuspenseQuery依赖Suspense边界处理加载状态,但未提前处理查询返回空数据的场景,导致空数据直接触发全局错误。
解决方法
1. 给查询函数添加默认返回值
修改getUserReplyData,当查询不到用户的回复状态时,返回一个默认的空状态对象,避免返回undefined:
async function getUserReplyData(toolName, userEmail) { if (!userEmail) return { liked: false, disliked: false }; const { data, error } = await supabase .from('user_replies') .select('liked, disliked') .eq('tool_name', toolName) .eq('user_email', userEmail) .single(); // 查询无结果或出错时返回默认状态 if (error || !data) { return { liked: false, disliked: false }; } return data; }
2. 未登录时跳过查询逻辑
在<ReplyItemOperations>组件中,判断用户未登录时直接渲染默认状态的按钮,不执行useSuspenseQuery:
import { useSuspenseQuery } from '@tanstack/react-query'; function ReplyItemOperations({ toolName, userEmail }) { // 未登录时直接渲染默认按钮 if (!userEmail) { return ( <div className="reply-operations"> <button>点赞</button> <button>点踩</button> <button>回复</button> </div> ); } const { data } = useSuspenseQuery({ queryKey: ["userReplyData", toolName, userEmail], queryFn: () => getUserReplyData(toolName, userEmail), }); return ( <div className="reply-operations"> <button className={data.liked ? 'active' : ''}>点赞</button> <button className={data.disliked ? 'active' : ''}>点踩</button> <button>回复</button> </div> ); }
3. 给页面添加错误边界
在<ToolPage>或其上层组件中添加React错误边界,捕获useSuspenseQuery抛出的错误,避免整个页面崩溃:
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { console.error('评论状态加载错误:', error, errorInfo); } render() { if (this.state.hasError) { return <div>加载评论状态失败,请刷新重试</div>; } return this.props.children; } } // 在ToolPage中包裹内容 function ToolPage() { return ( <ErrorBoundary> {/* 工具详情、评论列表等页面内容 */} </ErrorBoundary> ); }
4. 配置Query的兜底处理
给useSuspenseQuery添加select函数,兜底处理空数据,同时调整缓存参数避免冲突:
const { data } = useSuspenseQuery({ queryKey: ["userReplyData", toolName, userEmail], queryFn: () => getUserReplyData(toolName, userEmail), staleTime: 5 * 60 * 1000, // 5分钟缓存有效期 select: (data) => data || { liked: false, disliked: false }, // 空数据兜底 });
内容的提问来源于stack exchange,提问作者HKS

