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

Next.js服务端组件转客户端组件引发无限循环等异常问题

解决Next.js客户端组件搭配异步服务端组件的无限循环与状态异常问题

问题根源

你遇到的核心问题是**客户端组件(HomePagePosts)直接渲染异步服务端组件(PostCard)**的错误用法:

  • Next.js App Router不允许在客户端组件的同步渲染流程中调用异步服务端组件,这会触发React反复尝试渲染组件,进而导致无限循环、状态更新异常和页面卡顿。
  • 原代码中useEffect依赖项为空,无法响应categorySlug的变化,同时异步组件的渲染冲突进一步放大了状态管理的问题。

修复方案

步骤1:将异步逻辑上移到父组件

把PostCard中await myPortfolioURL的异步操作迁移到HomePagePosts中,提前批量获取作者的portfolio URL,避免PostCard成为异步组件。

修改后的HomePagePosts代码:

'use client'

import { useEffect, useState } from 'react'
import { getCategoryPosts, getPosts, myPortfolioURL } from '@/services'
import styles from '../../app/page.module.scss'
import PostCard from './PostCard'
import { postsType } from '@/utils/types/types'

type Props = { categorySlug?: string }

function HomePagePosts({ categorySlug = '' }: Props) {
  const [postsWithAuthorUrl, setPostsWithAuthorUrl] = useState<(postsType & { authorId: string })[]>([])

  useEffect(() => {
    async function fetchPosts() {
      let rawPosts: postsType[]
      if (!categorySlug) {
        rawPosts = await getPosts()
      } else {
        rawPosts = await getCategoryPosts(categorySlug)
      }
      // 批量获取作者portfolio URL
      const postsWithUrl = await Promise.all(
        rawPosts.map(async (post) => ({
          ...post,
          authorId: (await myPortfolioURL(post.author.id)) || '/'
        }))
      )
      setPostsWithAuthorUrl(postsWithUrl)
    }
    fetchPosts()
  }, [categorySlug]) // 添加categorySlug依赖,确保分类切换时重新获取数据

  const { postsContainer } = styles

  return (
    <section className={postsContainer}>
      {postsWithAuthorUrl.map((post) => (
        <PostCard post={post} key={post.title} />
      ))}
    </section>
  )
}

export default HomePagePosts

步骤2:修改PostCard为非异步客户端组件

移除PostCard中的async/await,直接使用父组件传入的authorId:

import { postsType } from '@/utils/types/types'
import styles from '../../app/page.module.scss'
import moment from 'moment'
import Link from 'next/link'
import Image from 'next/image'
import LikeButton from '../buttons/LikeButton'
import CommentButton from '../buttons/CommentButton'

// 扩展类型,包含authorId字段
type PostWithAuthorUrl = postsType & { authorId: string }

type Props = { post: PostWithAuthorUrl }

// 改为同步组件
function PostCard({ post }: Props) {
  const {
    postCard,
    postImage,
    title,
    summary,
    authorName,
    authorInfo,
    authorInfoContainer,
    authorImage,
    date,
    userFeedbackContainer,
    leftAlign,
    rightAlign,
    readTimeContainer,
    postReactionSection,
  } = styles
  const authorId = post.authorId

  return (
    <div className={postCard}>
      <div className={authorInfoContainer}>
        <Link
          href={authorId}
          className={authorInfo}
          rel="noopener noreferrer"
          target="_blank"
        >
          <div className={authorImage}>
            <Image
              src={post.author.photo.url}
              style={{ objectFit: 'cover', borderRadius: '50%' }}
              sizes="(max-width: 768px) 40px, (max-width: 1200px) 50px, 40px"
              fill={true}
              alt={post.author.name}
            />
          </div>
          <p className={authorName}>{post.author.name}</p>
        </Link>

        <div className={date}>
          <p>{moment(post.createdAt).format('MMM DD, YYYY')}</p>
        </div>
      </div>
      <Link href={`/post/${post.slug}`}>
        <div className={postImage}>
          <Image
            src={post.featuredImage.url}
            style={{ objectFit: 'cover', borderRadius: '.5rem' }}
            fill={true}
            alt={post.title}
          />
        </div>
      </Link>

      <h1>
        <Link href={`/post/${post.slug}`} className={title}>
          {post.title}
        </Link>
      </h1>
      <div className={summary}>{post.summary}</div>
      <div className={postReactionSection}>
        <div className={leftAlign}>
          <div className={userFeedbackContainer}>
            <LikeButton postId={post.id} />
            <CommentButton postId={post.id} />
          </div>
        </div>
        <div className={rightAlign}>
          <div className={readTimeContainer}>{post.readTime} min read</div>
        </div>
      </div>
    </div>
  )
}

export default PostCard

可选优化:用SWR管理数据(更适合分页场景)

如果后续要实现分页功能,推荐使用SWR替代useEffect+useState,它自带缓存、重新验证和分页支持,稳定性更高:

'use client'

import useSWR from 'swr'
import { getCategoryPosts, getPosts, myPortfolioURL } from '@/services'
import styles from '../../app/page.module.scss'
import PostCard from './PostCard'
import { postsType } from '@/utils/types/types'

type Props = { categorySlug?: string }

// 统一数据获取逻辑
const fetcher = async (categorySlug: string) => {
  let rawPosts: postsType[]
  if (!categorySlug) {
    rawPosts = await getPosts()
  } else {
    rawPosts = await getCategoryPosts(categorySlug)
  }
  return Promise.all(
    rawPosts.map(async (post) => ({
      ...post,
      authorId: (await myPortfolioURL(post.author.id)) || '/'
    }))
  )
}

function HomePagePosts({ categorySlug = '' }: Props) {
  const { data: postsWithAuthorUrl, error } = useSWR(categorySlug, fetcher)

  const { postsContainer } = styles

  if (error) return <div>加载失败</div>
  if (!postsWithAuthorUrl) return <div>加载中...</div>

  return (
    <section className={postsContainer}>
      {postsWithAuthorUrl.map((post) => (
        <PostCard post={post} key={post.title} />
      ))}
    </section>
  )
}

export default HomePagePosts

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 08:14:56