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

Next.js服务端渲染「查看更多/收起」实现及组件重复问题解决

问题解决:客户端组件控制Server Component渲染数量且避免重复渲染

问题背景

需要在客户端组件中控制渲染指定数量的Server Component(ServiceCard),所有数据已在数组中。已知直接在客户端环境插入Server Component会自动转为Client Component,需避免此情况。当前代码因ServicesSection和ServiceVisibilityController各有一次map操作,导致服务卡片重复渲染。

错误原因

原代码中,ServicesSection通过map生成所有ServiceCard作为children传给ServiceVisibilityController,而ServiceVisibilityController又对serviceItems做切片后再次map,每个motion.div中都渲染了全部children,最终每个动画容器里都包含所有服务卡片,造成重复渲染。

解决方案

核心思路:让客户端组件只负责控制可见数量、动画逻辑,将Server Component的渲染逻辑交回给Server环境的父组件,通过render prop的方式传递渲染函数,确保ServiceCard始终在Server端渲染,不被转为Client Component。

修正后的代码

1. 修改ServiceVisibilityController(客户端组件)

"use client";

import React, { useState, useRef, useEffect, Fragment } from "react";
import { motion, AnimatePresence } from "framer-motion";
import RenderButton from "./RenderButton";

interface ServiceVisibilityControllerProps {
  serviceItems: {
    id: number;
    imageSrc: {
      url: string;
      alt: string;
    }[];
    title: string;
    description: string[];
  }[];
  // 接收渲染单个卡片的函数,由父组件提供Server Component的渲染逻辑
  renderCard: (item: typeof serviceItems[0]) => React.ReactNode;
}

const ServiceVisibilityController: React.FC<ServiceVisibilityControllerProps> = ({
  serviceItems,
  renderCard,
}) => {
  const [visibleServices, setVisibleServices] = useState(6);
  const placeholderRef = useRef<HTMLDivElement>(null);

  const handleShowMore = () => {
    setVisibleServices(serviceItems.length);
  };

  const handleHide = () => {
    setVisibleServices(6);
  };

  useEffect(() => {
    if (visibleServices === 6 && placeholderRef.current) {
      placeholderRef.current.scrollIntoView({
        behavior: "smooth",
        block: "end",
      });
    }
  }, [visibleServices]);

  return (
    <Fragment>
      <div className="md:grid md:grid-cols-2 md:gap-5 lg:grid-cols-3">
        <AnimatePresence mode="sync">
          {serviceItems.slice(0, visibleServices).map((item, index) => (
            <motion.div
              key={item.id}
              className="scroll-mb-[150px]"
              initial={{ opacity: 0, y: 20 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0, y: -20 }}
              transition={{ delay: index * 0.05 }}
              ref={index === 5 ? placeholderRef : null}
            >
              {/* 调用父组件传入的渲染函数,渲染单个Server Component */}
              {renderCard(item)}
            </motion.div>
          ))}
        </AnimatePresence>
      </div>
      {visibleServices < serviceItems.length ? (
        <RenderButton onClick={handleShowMore} text="Показати більше" />
      ) : (
        <RenderButton onClick={handleHide} text="Сховати" />
      )}
    </Fragment>
  );
};

export default ServiceVisibilityController;

2. 修改ServicesSection(Server组件)

import { serviceItems } from "@/data";
import React from "react";
import SectionHeader from "./UI/SectionHeader";
import ServiceVisibilityController from "./ServiceVisibilityController";
import ServiceCard from "./ServiceCard";

const ServicesSection = () => {
  return (
    <section id="services" className="relative mb-24">
      <SectionHeader>Service</SectionHeader>
      <ServiceVisibilityController
        serviceItems={serviceItems}
        // 传递渲染单个ServiceCard的函数,此逻辑在Server环境执行
        renderCard={(item) => (
          <ServiceCard
            key={item.id}
            imageSrc={item.imageSrc}
            title={item.title}
            description={item.description}
          />
        )}
      />
    </section>
  );
};

export default ServicesSection;

关键说明

  • 避免Server Component转Client: 通过renderCard函数将ServiceCard的渲染逻辑保留在Server环境的父组件中,客户端组件仅负责调用该函数,确保ServiceCard始终以Server Component身份渲染。
  • 解决重复渲染: 移除了父组件中多余的map操作,仅在客户端组件中根据可见数量执行一次map,每个动画容器仅渲染对应的单个服务卡片。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 16:32:02