在SSR页面中使用客户端组件包裹器是否为合理实践?
这种做法完全符合SSR架构的最佳实践,核心思路抓得很准:服务端负责所有数据预取,客户端组件只专注于渲染和交互逻辑,完美实现了服务端与客户端的职责分离。
你的方案合理性
你的ClientSideWrapper本质是一个极简的客户端边界组件,通过"use client"指令把包裹的子组件纳入客户端渲染范畴,这是Next.js等SSR框架推荐的方式——既让服务端完成数据获取、提升首屏加载速度和SEO表现,又能让依赖浏览器API(比如事件监听、状态管理)的客户端组件正常工作。
可以优化的细节
减少冗余的Wrapper包裹
你现在给每个Accordion都单独套了ClientWrapper,如果Accordion本身已经是带"use client"的组件,或者父级已经有客户端边界,重复包裹不会报错,但会增加不必要的组件层级。可以把多个客户端组件放在同一个ClientWrapper下,比如把整个accordion列表的循环放在一个Wrapper里。让Wrapper更实用
目前你的Wrapper只是单纯透传children,其实可以给它加些额外逻辑,比如客户端挂载时的初始化操作、加载状态兜底、错误捕获等,让它从“空壳组件”变成真正能复用的工具组件。严格保持职责分离
继续坚持服务端做所有数据请求和处理(比如你现在的getResources()、filterAndMapTags这些操作),客户端组件只接收预好的数据做渲染和交互,不要在客户端组件里再发起数据请求,避免出现首屏数据缺失或重复请求的问题。
总结
你的方案是规范且高效的,是SSR架构中处理客户端组件的标准方式之一,既发挥了服务端渲染的优势,又能无缝集成需要浏览器环境的交互组件。
你提供的代码示例
ClientSideWrapper
"use client"; interface ClientWrapperProps { children: React.ReactNode; } export const ClientWrapper = ({ children }: ClientWrapperProps) => { return <> {children} </>; };
page.tsx
export default async function ResourcesPage({ params, }: { params: { slug: string }; }) { const resources = await getResources(); console.log("Resources", JSON.stringify(resources)); const tags = await getContentfulTags(); const productTags = filterAndMapTags(tags, "Product:"); const industryTags = filterAndMapTags(tags, "Industry"); const applicationTags = filterAndMapTags(tags, "Application"); const resourceTypeTags = filterAndMapTags(tags, "ResourceType"); const resourcesByType: { [key: string]: TransformedResource[] } = {}; resourceTypes.forEach((resourceType) => { resourcesByType[resourceType] = resources.filter( (item) => item.resourceType === resourceType ); }); const accordionData = [ { title: "Drop-in Specs", data: resourcesByType["Drop-in Specs"], results: resourcesByType["Drop-in Specs"].length, }, { title: "Installation Manuals", data: resourcesByType["Installation Manuals"], results: resourcesByType["Installation Manuals"].length, }, { title: "Brochures", data: resourcesByType["Brochures"], results: resourcesByType["Brochures"].length, }, { title: "Declarations of Perfromance", data: resourcesByType["Declarations of Performance"], results: resourcesByType["Declarations of Performance"].length, }, { title: "Application Sheets", data: resourcesByType["Application Sheets"], results: resourcesByType["Application Sheets"].length, }, { title: "Product Sheets", data: resourcesByType["Product Sheets"], results: resourcesByType["Product Sheets"].length, }, { title: "Technical Notes", data: resourcesByType["Technical Notes"], results: resourcesByType["Technical Notes"].length, }, { title: "DOTs/MTOs", data: resourcesByType["DOTs/MTOs"], results: resourcesByType["DOTs/MTOs"].length, }, { title: "Tools", data: resourcesByType["Tools"], results: resourcesByType["Tools"].length, }, ]; const dropdownItems = [ { title: "Product", data: productTags }, { title: "Resource Type", data: resourceTypeTags }, { title: "Industry", data: industryTags }, { title: "Application", data: applicationTags }, ]; return ( <div className="overflow-hidden px-6 pb-6 pt-3 md:px-3 sm:px-2 xs:px-0"> <StaticBreadCrumb mappableProps={fakeObjectForBreadCrumb} headerImage={false} /> <h1 className="mb-4 mt-3 sm:mb-3">Resources</h1> <div className="flex flex-col"> <div className="w-100 relative flex flex-row justify-start border-b-[1px] border-softGrey pb-3 md:flex-col md:pb-4"> <h4 className="mr-1 text-mdLg text-softGrey ">Filter</h4> <ClientWrapper> <div className="relative"> <div className="no-scrollbar absolute ml-3 flex w-[100%] flex-row md:left-[0px] md:ml-[0px] md:mt-1 smMd:mt-[-25px] smMd:block"> <div className="flex flex-row smMd:invisible"> {dropdownItems.map((item, index) => ( <div key={index} className="z-50 ml-3 md:ml-[0px] md:mr-1"> <Dropdown title={item.title}> <ResourceDropDown data={item.data} /> </Dropdown> </div> ))} </div> <div className="lg:invisible md:invisible smMd:visible"> <MobileResourceDropDown dropdownItems={dropdownItems}> <ResourceDropDown data={productTags} /> </MobileResourceDropDown> </div> </div> </div> </ClientWrapper> </div> <div className=" mt-3 flex flex-col px-3 smMd:px-2 sm:px-0"> {accordionData.map((item, index) => { return ( <ClientWrapper key={index}> <Accordion title={item.title} results={item.results} key={index} > <div className={`${index === 1 ? "mt-3" : "mt-1"}`}> <AccordionResource accordionData={item.data} /> </div> </Accordion> </ClientWrapper> ); })} </div> </div> </div> ); }
内容的提问来源于stack exchange,提问作者michaelNL

