StudioComposition and Slot props
Look up the props of <StudioComposition />, which renders a composition against data you already hold, and <Slot>, which passes data from your component to whatever an author drops into one of its slots. Both get their data from your code, and the SDK doesn't fetch it. For how to use them, see Bring your own data.
The example on this page is a Blog post content type whose entries live under /blog. Your blog index route already fetches its posts. It renders a Post card section, with the Section UID post_card, once per post. The entry AI 101 has the title AI 101 and the URL /blog/ai-101. The names come from the team blog.
StudioComposition props
interface StudioCompositionProps {
context: unknown;
spec?: StudioSpec;
compositionUid?: string;
loadingFallback?: React.ReactNode;
errorFallback?: React.ReactNode;
}
| Prop | Type | Required | Default | What it does |
|---|---|---|---|---|
context | unknown | Yes | None | The data the composition renders against. Bindings to the template entry read from it. Pass the object as you hold it, such as one post. |
spec | StudioSpec | One of spec or compositionUid | None | A composition you fetched with sdk.fetchComposition. When you pass it, the component ignores compositionUid and loads nothing. |
compositionUid | string | One of spec or compositionUid | None | The UID of a composition to fetch in the browser. It works only on the client. |
loadingFallback | React.ReactNode | No | null | What renders while the compositionUid fetch runs. |
errorFallback | React.ReactNode | No | null | What renders when the compositionUid fetch fails. The error is also logged to the console. |
<StudioComposition /> renders without canvas editing, and it works in client and server rendering.
Where StudioComposition gets the composition
| You pass | What happens | Use it when |
|---|---|---|
spec | It renders straight away. | You render on the server, or you've already fetched the composition. |
compositionUid | It fetches the composition in the browser after it mounts, and shows loadingFallback until then. | You render on the client and haven't fetched it. |
| Neither | It throws. | Never |
With neither prop, it throws this error.
[Contentstack Studio] StudioComposition requires either a `spec` or a `compositionUid`.
compositionUid doesn't work during a server render, because the fetch runs in the browser. On the server, fetch the composition first and pass spec.
When you pass compositionUid, studioSdk.init has to run before the component mounts. If it hasn't, the fetch fails and errorFallback renders. The console logs StudioComposition: failed to fetch composition, with an error that says SDK not initialized. Call studioSdk.init(config) before rendering with a compositionUid.
Fetch the composition with sdk.fetchComposition
sdk.fetchComposition(query: CompositionQueryForSSRInput, options?: CompositionQueryOptions): Promise<StudioSpec>
It fetches the composition's layout, its entry and the sections it uses. It skips the data, so spec.data comes back empty. context supplies the data instead. It runs on the server and in the browser. It takes the same query and options as sdk.fetchCompositionData, which are listed in SDK API.
// app/blog/page.tsx, a server component
import { StudioComposition } from "@contentstack/studio-react";
import { sdk } from "@/lib/studio";
export default async function BlogIndex() {
const [posts, spec] = await Promise.all([
getPosts(),
sdk.fetchComposition({ compositionUid: "post_card", searchQuery: "" }),
]);
return posts.map((post) => (
<StudioComposition key={post.uid} spec={spec} context={post} />
));
}
For AI 101, context is { uid, title: "AI 101", url: "/blog/ai-101" }, and a heading in the Post card bound to the entry's title shows AI 101.
How StudioComposition uses context
| Case | What happens |
|---|---|
A binding to the template entry, such as title | Reads that field from context. Pass context in the shape the composition was built against, with the same field names. |
| A section placed inside the composition | Gets its data from context too, with no extra fetch. |
context isn't data. <StudioComponent /> takes a data prop for values that aren't in Contentstack, and <StudioComposition /> takes context in place of the template entry.
SEO values from your own data
resolveSeoMetadata(spec: StudioSpec, context?: unknown): SeoMetadata | null
It fills in a template's SEO values from context. Use it for the head of a route you render with <StudioComposition />. It returns pageTitle, pageDescription, openGraphImage and metaProps, or null when the composition has no SEO values. A section has none, so it always returns null. See SEO and metadata.
Slot props
interface SlotProps {
data?: Record<string, unknown>;
children: React.ReactNode;
}
| Prop | Type | Required | Default | What it does |
|---|---|---|---|---|
data | Record<string, unknown> | No | None | Values the components dropped into this slot can bind. Each top-level key becomes a field an author can pick. |
children | React.ReactNode | Yes | None | The slot's value. This is the prop your component receives for the slot, such as ctaSlot. |
In the example, your Post card component has a slot prop ctaSlot. It wraps the slot in <Slot> so a Button an author drops there can link to the post.
import { Slot, type SlotProps } from "@contentstack/studio-react";
interface PostCardProps {
url: string;
ctaSlot?: SlotProps["children"];
}
export function PostCard({ url, ctaSlot }: PostCardProps) {
return (
<article>
<Slot data={{ url }}>{ctaSlot}</Slot>
</article>
);
}
For AI 101, the Button's link bound to url becomes /blog/ai-101.
How Slot data reaches the components inside it
| Case | What happens |
|---|---|
| A component dropped into the slot | Can bind every key in data. |
Your app also passes data to <StudioComponent /> | The slot's keys are added to those values. Where a key is in both, the slot's value wins. |
| A slot inside another slot | The inner slot's keys win over the outer one's. |
You render {ctaSlot} without <Slot> | The slot works, and carries no data. |
| Where the data is kept | Nowhere. <Slot> passes it at render time, on your site and in Studio, and nothing is saved in the composition. |
<Slot> is the component you render. The slot prop type, declared in your component's schema, is what makes the drop zone. See Component schema: props, types and defaults.
Slot data reaches Studio as plain values, with no content type behind it. A Repeater inside the slot that loops over a slot key has no schema, so Studio can't tell which content type or block each item is. To fix that, declare dataFrom on the slot prop for a key that holds another prop's bound list, unchanged or only sliced, filtered or sorted. With dataFrom: { items: "source" }, Studio takes the schema for the items key from whatever your component's source prop is bound to. It changes only the schema, not the data, and nothing is saved.
What authors see when they bind slot data
On the canvas, an author selects the Button inside the slot and binds its link. The slot's keys, such as url, are listed under Component Props in the Component Data source.
A slot's keys show only for a component selected inside that slot. With nothing selected in the slot, or with a component in another slot selected, they're not listed.
Where to go next
- Render a composition from data you hold, step by step: Bring your own data.
- Declare slot props on a component: Component schema: props, types and defaults.
- Look up the rest of the SDK: SDK API.