During the conversion process from JSS to Content SDK for Sitecore AI (IE XM Cloud), there's some changes to how components are designated and mapped to be aware of, which affect how placeholders work and how those components work inside the placeholders. This led to our needing a solution to handle placeholders in certain circumstances.
Server vs Client Components
The component designation in Content SDK separates server from client components. This separation is determined by the use of hooks in the code (like useEffect) and a component is made "client" by placing "use client" in the first line of the component. Technically you could make every component "client" but you lose out on benefits of server components.
When it comes to Sitecore, there's a useSitecore hook that can be used for getting things like the editing state. But you don't need to use that hook, because the new placeholder model, called AppPlaceholder, that passes the page model down from one level to the next. That includes the edit state, so you can access it in server components as well. This starts all the way up in your Layout.tsx file, which you'll see in the starter kit you can download from Github, and it's a required parameter so it'll always be passed down.
The Challenge
You cannot bring server components into a placeholder under a client component. Sitecore has documentation on this. It'll work if you set things up manually, but in Pages you won't be allowed to pull components in. To be clear, the scenario is you have a component - let's say a tabs component - that has an AppPlaceholder on it. The tabs component will typically have some hooks to handle click changes between the tabs, and that requires "use client" to be designated. Then you'll have a tabs panel component, which you pull in to set up each individual tab, and that also has AppPlaceholder to hold a variety of other components. Then you might pull in a rich text component, which is likely a server component because it has no hooks. If you just do that straight away, like you would have in JSS, you won't be able to bring those server components in.
A Solution
I'll say a solution because I'm sure there's others, and the documentation suggests something, but it wasn't entirely clear to me. What I worked out with some research, AI, and trial and error, was this solution. The solution boils down to this: if you have a component that has AppPlaceholder and needs to be a client component as well, you have a client/server tag team.
The Server Component
The server component part is the actual component you'll be registering in Sitecore as a rendering. It might look something like this:
import { type JSX } from "react";
import { AppPlaceholder } from "@sitecore-content-sdk/nextjs";
import componentMap from ".sitecore/component-map";
import TabsClient from "./TabsClient";
import type { TabsProps } from "./Tabs.types";
/**
* Tabs — server component wrapper.
*
* Owns AppPlaceholder so the Sitecore editor treats the placeholder as
* server-owned and allows inserting any component type (server or client).
*
* Edit mode: renders the placeholder in a single column with a label.
*
* Normal mode: passes pre-rendered placeholder content as RSC children to
* TabsClient, which handles mobile detection and DOM manipulation for the
* horizontal tab navigation.
*/
export default function Tabs({ rendering, params, page }: TabsProps): JSX.Element {
const isPageEditing = page?.mode?.isEditing ?? false;
const phKey = `tabs-${params?.DynamicPlaceholderId ?? ""}`;
const content = (
<AppPlaceholder
name={phKey}
rendering={rendering}
componentMap={componentMap}
page={page}
/>
);
if (isPageEditing) {
return (
<>
<section
id={params?.RenderingIdentifier ?? ""}
className={`component tabs ${params?.Styles ?? ""}`}
>
<div className="container">
<div className="col-12">
<h3>Tabs (edit mode)</h3>
{content}
</div>
</div>
</section>
</>
);
}
return (
<>
<TabsClient
renderingUid={rendering.uid ?? ""}
params={params}
>
{content}
</TabsClient>
</>
);
}
You'll notice that this takes an AppPlaceholder component and sends it to a component called TabsClient. That's what we'll look at next, and where all the hooks are working at. But this provides a mechanism by which we continue the "chain" of server components, which allows us to use server or client components in the subsequent placeholder tree.
The Client Component
In our case, the tabs component is designed to switch to an accordion if in mobile mode, so there's a useEffect and other hooks to handle resize checks. It also takes that children parameter, which is our AppPlaceholder fed down from the server component, and now it's safe to use server or client components. You can see all this in the code below.
"use client";
import {
useCallback,
useEffect,
useRef,
useState,
type JSX,
type ReactNode,
} from "react";
import type { TabsParams } from "./Tabs.types";
interface TabsClientProps {
children: ReactNode;
renderingUid: string;
params?: TabsParams;
}
/**
* TabsClient — handles mobile detection and horizontal tab nav DOM
* manipulation for Tabs. Receives pre-rendered placeholder content as
* children from the server component wrapper.
*
* After mount, moves .navItem <li> elements from the content area into the
* nav <ul> and activates the first tab.
*/
export default function TabsClient({
children,
renderingUid,
params,
}: TabsClientProps): JSX.Element {
const containerRef = useRef<HTMLUListElement>(null);
const [isMobile, setIsMobile] = useState(false);
const handleResize = useCallback(() => {
setIsMobile(window.innerWidth < 576);
}, []);
useEffect(() => {
handleResize();
window.addEventListener("resize", handleResize);
if (!isMobile) {
const runMove = () => {
const elements = document.querySelectorAll(
`.content-${renderingUid} .navItem`,
);
elements.forEach((el) => {
if (el === elements[0]) {
const anchor = el.getElementsByTagName("a")[0];
anchor?.classList.add("active");
anchor?.setAttribute("aria-selected", "true");
const panel = document.querySelector(
`.content-${renderingUid} .tab-pane`,
);
panel?.classList.add("active");
panel?.classList.add("show");
}
containerRef.current?.appendChild(el);
});
};
requestAnimationFrame(runMove);
}
return () => {
window.removeEventListener("resize", handleResize);
};
}, [handleResize, isMobile, renderingUid]);
if (isMobile) {
return (
<section className="component accordions" data-module="accordions">
<div className="container">
<div className="box-shadow">{children}</div>
</div>
</section>
);
}
return (
<section
id={params?.RenderingIdentifier ?? ""}
className={`component tabs ${params?.Styles ?? ""}`}
>
<div className="container">
<div className="row">
<div className="col-12">
<ul
className={`nav nav-tabs tabs-${renderingUid}`}
ref={containerRef}
role="tablist"
/>
<div className={`tab-content content-${renderingUid}`}>
{children}
</div>
</div>
</div>
</div>
</section>
);
}
Conclusion
This will hopefully provide developers with a starting point for handling placeholders where a client component is required. It should be pretty straightforward to engineer in your handling as appropriate, or to feed the example to AI to help you generate what you need.
Remember, if your AppPlaceholder component doesn't require hooks, you don't need to do any of this extra work. Keep it as simple as possible!
For further actions, you may consider blocking this person and/or reporting abuse
