render-with-own-data
When to use
Render a Studio composition (page or standalone section) against data you already hold, via <StudioComposition context={...} /> plus the spec-only sdk.fetchComposition.
Use when you already hold the data (a group-array element, a commerce record, a request-time value) and must render a composition against it with no SDK data fetch. Phrases: "render this section per array item", "section per product", "render a composition with my data". Do NOT use for route rendering: that's setup-template-preview-routes + wire-external-data.
Mandatory auth preflight: settle the credential before the first API call. Resolve it OAuth-first per
authenticate-cma:CS_OAUTH_ACCESS_TOKEN, else the Contentstack MCP's stored session: Read that skill for where it is, even with no MCP connected. Never ask the user for a sessionauthtoken. If nothing resolves, or a refresh fails with400 invalid_refresh_token, hand them! CONTENTSTACK_REGION=<code> npx @contentstack/mcp --auth(it needs a TTY and a browser, so it cannot be run for them) and wait.403 error_code 316is a valid credential aimed at another org: fix the org or theapi_key, do not re-authenticate.
Render a Composition With Your Own Data
Context
<StudioComposition /> renders a composition (a full page, or a section as the root) against a context object you pass in. No SDK data fetch, no hook, no route ownership. It runs in CSR and SSR.
You give it two things: which composition, and the data.
- Which composition: a pre-fetched
specfromsdk.fetchComposition, or acompositionUidit fetches on the client. - The data:
context, placed internally atdataSources.template, so the composition's existing template bindings resolve against it. Embedded sections are auto-scoped fromcontext(pure, no network). You never buildsection_scoped_data.
Reference: docs/15-build/15-data/32-render-a-composition-with-your-own-data.md, docs/30-look-it-up/05-reference/10-studiocomposition-and-slot-props.md.
This is not <StudioComponent />. That one owns a route and resolves composition and CMS data through sdk.fetchCompositionData. <StudioComposition /> owns a subtree and takes the data from you. If the choice of surface is still open, run understand-bring-your-own-data first.
Task
-
Prereq:
studioSdk.init(...)runs before render, and every component the composition uses is registered. Without init thecompositionUidpath logsStudioComposition: SDK not initialized. Call studioSdk.init(config) before rendering with a compositionUid.and renderserrorFallback.sdk.fetchCompositionthrows when nostackSdkis configured. Runinstall-studioandregister-componentif either is missing. -
Identify the composition. Get
compositionUidfrom the composition's URL in Studio.sdk.fetchCompositionalso acceptsurlortemplateContentTypeUid(same identifier shapes assdk.fetchCompositionData) plussearchQuery. PasssearchQuery: ""when rendering on your own site. It exists to forward Studio's iframe overrides (locale, variant, preview entry, edit mode). -
Shape
contextto the composition's bindings. A binding totitlereadscontext.title. A binding that iteratessub_itemsreadscontext.sub_items. Pass the raw element you hold (one item of a multiple-group field, one API record) and the SDK wraps it. If the composition was authored against aproducttemplate,contextis one product-shaped object with the same field names. -
Pick the fetch path by render strategy:
Situation Path SSR / RSC / Remix loader / getServerSidePropsPre-fetch spec: a"use client"component cannot fetch during a server renderA loop rendering N instances of the same composition Pre-fetch speconce above the loopA single client-rendered mount compositionUid+loadingFallback+errorFallback -
Pre-fetched spec (SSR, the default). Resolve the spec and your own data in parallel, render immediately, no client loading state:
// app/products/page.tsx — Next App Router Server Component (no "use client")import { sdk } from "@/lib/contentstack";import { StudioComposition } from "@contentstack/studio-react";export default async function ProductsPage() {const [products, spec] = await Promise.all([getProducts(), // your data sourcesdk.fetchComposition({ compositionUid: "product_tile", searchQuery: "" }),]);return (<div className="grid">{products.map((element, i) => (<StudioComposition key={i} spec={spec} context={element} />))}</div>);}sdk.fetchComposition(query, options?)returnsPromise<StudioSpec>whosedatais intentionally empty: that's the bring-your-own-data path.optionsis the sameCompositionQueryOptionsassdk.fetchCompositionData(variantAlias,locale,templateEntryUid,extendQuery, custom fetchers).Pages Router / Remix: call
sdk.fetchCompositioningetServerSideProps/loaderand passspecthrough props. -
compositionUidpath (client only). Always pair it with both fallbacks: the defaults arenull, so a failed fetch renders nothing:"use client";import { StudioComposition } from "@contentstack/studio-react";<StudioCompositioncompositionUid="hero_section"context={element}loadingFallback={<SectionSkeleton />}errorFallback={<NotFound />}/>;In a CSR app you can also pre-fetch in
useEffectand hold theStudioSpecin state, worth it when several mounts share one composition. -
Fetch the spec once per composition, never per item. Each
<StudioComposition compositionUid={…} />inside a loop fetches independently. Hoist the fetch, pass the samespecobject to every item. -
Keep
specandcontextJSON-serializable.<StudioComposition />is a"use client"component, so in RSC both props cross the server to client boundary. Class instances, functions,Map/Set, andDateobjects break serialization: pass plain objects, arrays, strings, numbers, booleans,null. -
Verify in a normal browser tab (not the Studio iframe): each mount renders the composition's own layout with your element's values, and item N shows item N's data.
Inputs needed from the user
In this order. If any is missing, ask before editing code.
compositionUid: which composition to render (or theurl/templateContentTypeUidthat resolves it).dataShape: the object you hand tocontext, so we can check it matches the composition's binding field names.renderPath: CSR or SSR. Picks step 5 vs step 6.mountFile: the file where<StudioComposition />mounts.
Acceptance
This skill succeeds only when ALL of the following are true. If any fails, surface the failure and stop.
-
mountFilerenders<StudioComposition />withcontextplus exactly one ofspec/compositionUid. - In SSR the spec is resolved with
sdk.fetchCompositionon the server and passed asspec, nocompositionUidon a server-rendered mount. -
contextfield names match the bindings the composition was authored against. - A loop fetches the spec once above it and reuses the same
specfor every item. - The
compositionUidpath passes bothloadingFallbackanderrorFallback. -
specandcontextare JSON-serializable end-to-end (no functions, class instances,Date/Map/Set). - Loading the route in a normal browser tab renders every mount with its own data, verified by screenshot or by reading the rendered values, not by the absence of console errors.
Common pitfalls
| Pitfall | Why it bites | Fix |
|---|---|---|
Neither spec nor compositionUid passed | The component throws StudioComposition requires either a 'spec' or a 'compositionUid'. | Pass one: spec for SSR, compositionUid for client-only |
compositionUid on a server-rendered mount | The fetch is a client effect. The server renders nothing and content pops in late | Pre-fetch with sdk.fetchComposition, pass spec |
Passing data={...} instead of context={...} | data is the <StudioComponent /> prop. <StudioComposition /> ignores it and renders with no template data | Use context, see understand-bring-your-own-data § Why context is not data |
context shape doesn't match the bindings | Bound nodes render blank with no error | Pass the element in the shape the composition was authored against (same field names) |
Expecting spec.data to be populated | fetchComposition skips the data fetch by design | Supply the data through context |
sdk.public.fetchComposition(...) | studioSdk.init(...) already returns the public surface | Call sdk.fetchComposition(...) |
| Spec fetched inside the loop body | One network call per item | Hoist the fetch above the loop |
No loadingFallback / errorFallback | Both default to null: a failed or slow fetch renders an empty gap | Pass both on the compositionUid path |
Non-serializable context in RSC | Next.js rejects the prop crossing the client boundary | Map to a plain object before passing |
compositionUid path with no studioSdk.init | Console error SDK not initialized, then errorFallback | Initialize the SDK at app startup (install-studio) |
| Composition renders unstyled | Design tokens come from spec.config via the composition itself | Confirm the composition resolves (a real spec), and that breakpoints/tokens are registered (import-design-tokens) |
See also
docs/15-build/15-data/32-render-a-composition-with-your-own-data.md: CSR/SSR/loop walkthrough + per-framework fetch locationsdocs/30-look-it-up/05-reference/10-studiocomposition-and-slot-props.md: every prop, thefetchCompositionquery shape, andoptionsunderstand-bring-your-own-data: which of the three data surfaces fitswire-slot-data: the other bring-your-own-data surface (per-slot)embed-composition: mount a composition inside a code-owned page when the SDK should fetch its data tooconfigure-csr-vs-ssr: pick the render path before wiring step 5 vs 6