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
相关产品推荐
相关产品推荐

