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

NextJS客户端调用MongoDB Server Action触发栈溢出错误排查

解决帖子回复组件无限滚动的栈溢出及第二页加载失败问题

问题分析

开发帖子回复展示组件时,尝试用react-intersection-observer实现无限滚动出现Maximum call stack size exceeded错误,改为按钮触发加载第二页后仍无法正常获取数据。核心问题集中在Server Action的调用方式、客户端状态管理及props传递三个方面。

修复步骤

1. 标记Server Action为服务端执行

Next.js中Server Action必须显式声明'use server'指令,否则客户端会将其当作本地函数执行,引发调用栈溢出或请求失败。修改你的Server Action代码:

'use server'; // 必须添加此行,标记为服务端执行函数

export async function getAnswers(params: GetAnswersParams) {
  try {
    connectToDatabase();

    const { questionId, sortBy, page = 1, pageSize = 10 } = params;
    const skipAmount = (page - 1) * pageSize;

    let sortOptions = {};
    switch (sortBy) {
      case 'highestUpvotes': sortOptions = { upvotes: -1 }; break;
      case 'lowestUpvotes': sortOptions = { upvotes: 1 }; break;
      case 'recent': sortOptions = { createdAt: -1 }; break;
      case 'old': sortOptions = { createdAt: 1 }; break;
      default: break;
    }

    const answers = await Answer.find({ question: questionId })
      .populate('author', '_id clerkId name picture')
      .sort(sortOptions)
      .skip(skipAmount)
      .limit(pageSize);

    const totalAnswers = await Answer.countDocuments({ question: questionId });
    const totalPages = Math.ceil(totalAnswers / pageSize);
    const isNext = totalAnswers > skipAmount + answers.length;

    return { answers, isNext, totalPages };
  } catch (error) {
    console.log(error);
    throw error;
  }
}

2. 客户端维护页码状态,避免硬编码

原客户端代码中硬编码page:2,只能重复加载第二页,且未合并数据。修改LoadMoreAnswers组件,添加页码跟踪状态:

const LoadMoreAnswers = ({ initialAnswers, questionId, userId, filter }: Props) => {
  const [allAnswers, setAllAnswers] = useState(JSON.parse(initialAnswers));
  const [currentPage, setCurrentPage] = useState(1);
  const [isLoading, setIsLoading] = useState(false);

  const loadMoreAnswers = async () => {
    if (isLoading) return; // 防止重复点击触发请求
    setIsLoading(true);
    
    try {
      const nextPage = currentPage + 1;
      const { answers } = await getAnswers({
        questionId, // 直接使用props传递的questionId
        page: nextPage,
        sortBy: filter || 'recent', // 使用传入的筛选条件,而非硬编码
      });

      // 合并新旧回复数据
      setAllAnswers(prevAnswers => [...prevAnswers, ...answers]);
      setCurrentPage(nextPage);
    } catch (error) {
      console.error('Error loading more answers:', error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <>
      {allAnswers.map((answer) => (
        <article key={answer._id} className='light-border border-b py-10'>
          <div className='mb-8 flex flex-col-reverse justify-between gap-5 sm:flex-row sm:items-center sm:gap-2'>
            <Link
              href={`/profile/${answer.author.clerkId}`}
              className='flex flex-1 items-start gap-1 sm:items-center'
            >
              <Image
                src={answer.author.picture}
                alt='profile picture'
                width={18}
                height={18}
                className='rounded-full object-cover max-sm:mt-0.5'
              />
              <div className='flex flex-col sm:flex-row sm:items-center'>
                <p className='body-semibold text-dark300_light700'>{answer.author.name}</p>
                <p className='small-regular text-dark400_light500 mt-0.5 line-clamp-1'>
                  <span className='mx-0.5 max-sm-hidden'>•</span>
                  {/* answered {getTimestamp(answer.createdAt)} */}
                </p>
              </div>
            </Link>
            <div className='flex justify-end'>
              <Votes
                type='Answer'
                itemId={JSON.stringify(answer._id)}
                userId={userId}
                upvotes={answer.upvotes.length}
                hasupVoted={answer.upvotes.includes(userId)}
                downvotes={answer.downvotes.length}
                hasdownVoted={answer.downvotes.includes(userId)}
              />
            </div>
          </div>
          <ParseHTML data={answer.content} />
        </article>
      ))}

      <Button onClick={loadMoreAnswers} disabled={isLoading}>
        {isLoading ? '加载中...' : '加载更多'}
      </Button>
    </>
  );
};

export default LoadMoreAnswers;

3. 优化Server到Client的Props传递

原代码中对questionId进行了不必要的JSON.stringify和JSON.parse,可能导致数据格式异常。修改AllAnswers组件,直接传递原始props:

const AllAnswers = async ({ questionId, userId, totalAnswers, page, filter }: Props) => {
  const { answers, isNext, totalPages } = await getAnswers({
    questionId,
    page: page ? +page : 1,
    sortBy: filter,
  });

  return (
    <div className='mt-11'>
      <div className='flex items-center justify-between'>
        <h3 className='primary-text-gradient'>{`${totalAnswers} ${totalAnswers === 1 ? 'Answer' : 'Answers'}`}</h3>
        <Filter filters={AnswerFilters} />
      </div>

      <LoadMoreAnswers
        initialAnswers={JSON.stringify(answers)}
        questionId={questionId} // 直接传递,无需序列化
        userId={userId}
        filter={filter}
      />
    </div>
  );
};

export default AllAnswers;

4. 修复原无限滚动的栈溢出问题(可选)

如果之后要恢复滚动加载,需通过加载状态防止重复触发请求:

import { useInView } from 'react-intersection-observer';

const LoadMoreAnswers = ({ initialAnswers, questionId, userId, filter }: Props) => {
  // ...其他状态
  const { ref, inView } = useInView({ threshold: 0.1 });

  useEffect(() => {
    if (inView && !isLoading) {
      loadMoreAnswers();
    }
  }, [inView, isLoading]);

  return (
    <>
      {/* ...回复列表渲染 */}
      <div ref={ref} className='mt-11 flex w-full items-center justify-center'>
        <Button onClick={loadMoreAnswers} disabled={isLoading}>
          {isLoading ? '加载中...' : '加载更多'}
        </Button>
      </div>
    </>
  );
};

关键说明

  • Server Action必须加'use server',否则客户端无法正确发起服务端请求。
  • 客户端需维护页码状态,确保每次加载下一页而非重复加载同一页。
  • 避免不必要的JSON序列化/反序列化,防止数据格式错误或性能损耗。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 04:18:10