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

在SSR页面中使用客户端组件包裹器是否为合理实践?

在SSR页面中用ClientSideWrapper包裹客户端组件是否符合开发规范?

这种做法完全符合SSR架构的最佳实践,核心思路抓得很准:服务端负责所有数据预取,客户端组件只专注于渲染和交互逻辑,完美实现了服务端与客户端的职责分离。

你的方案合理性

你的ClientSideWrapper本质是一个极简的客户端边界组件,通过"use client"指令把包裹的子组件纳入客户端渲染范畴,这是Next.js等SSR框架推荐的方式——既让服务端完成数据获取、提升首屏加载速度和SEO表现,又能让依赖浏览器API(比如事件监听、状态管理)的客户端组件正常工作。

可以优化的细节

  1. 减少冗余的Wrapper包裹
    你现在给每个Accordion都单独套了ClientWrapper,如果Accordion本身已经是带"use client"的组件,或者父级已经有客户端边界,重复包裹不会报错,但会增加不必要的组件层级。可以把多个客户端组件放在同一个ClientWrapper下,比如把整个accordion列表的循环放在一个Wrapper里。

  2. 让Wrapper更实用
    目前你的Wrapper只是单纯透传children,其实可以给它加些额外逻辑,比如客户端挂载时的初始化操作、加载状态兜底、错误捕获等,让它从“空壳组件”变成真正能复用的工具组件。

  3. 严格保持职责分离
    继续坚持服务端做所有数据请求和处理(比如你现在的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 07:57:33