Studio Docs

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. Never ask the user for a session authtoken. If nothing resolves, or a refresh fails with 400 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 316 is a valid credential aimed at another org: fix the org or the api_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 spec from sdk.fetchComposition, or a compositionUid it fetches on the client.
  • The data: context, placed internally at dataSources.template, so the composition’s existing template bindings resolve against it. Embedded sections are auto-scoped from context (pure, no network). You never build section_scoped_data.

Reference: docs/bring-your-own-data/studio-composition.md, docs/bring-your-own-data/studio-composition-props-reference.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

  1. Prereq: studioSdk.init(...) runs before render, and every component the composition uses is registered. Without init the compositionUid path logs StudioComposition: SDK not initialized. Call studioSdk.init(config) before rendering with a compositionUid. and renders errorFallback. sdk.fetchComposition throws when no stackSdk is configured. Run install-studio and register-component if either is missing.

  2. Identify the composition. Get compositionUid from the composition’s URL in Studio. sdk.fetchComposition also accepts url or templateContentTypeUid (same identifier shapes as sdk.fetchCompositionData) plus searchQuery. Pass searchQuery: "" when rendering on your own site. It exists to forward Studio’s iframe overrides (locale, variant, preview entry, edit mode).

  3. Shape context to the composition’s bindings. A binding to title reads context.title. A binding that iterates sub_items reads context.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 a product template, context is one product-shaped object with the same field names.

  4. Pick the fetch path by render strategy:

    SituationPath
    SSR / RSC / Remix loader / getServerSidePropsPre-fetch spec: a "use client" component cannot fetch during a server render
    A loop rendering N instances of the same compositionPre-fetch spec once above the loop
    A single client-rendered mountcompositionUid + loadingFallback + errorFallback
  5. 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 source
        sdk.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?) returns Promise<StudioSpec> whose data is intentionally empty: that’s the bring-your-own-data path. options is the same CompositionQueryOptions as sdk.fetchCompositionData (variantAlias, locale, templateEntryUid, extendQuery, custom fetchers).

    Pages Router / Remix: call sdk.fetchComposition in getServerSideProps / loader and pass spec through props.

  6. compositionUid path (client only). Always pair it with both fallbacks: the defaults are null, so a failed fetch renders nothing:

    "use client";
    import { StudioComposition } from "@contentstack/studio-react";
    
    <StudioComposition
      compositionUid="hero_section"
      context={element}
      loadingFallback={<SectionSkeleton />}
      errorFallback={<NotFound />}
    />;

    In a CSR app you can also pre-fetch in useEffect and hold the StudioSpec in state, worth it when several mounts share one composition.

  7. Fetch the spec once per composition, never per item. Each <StudioComposition compositionUid={…} /> inside a loop fetches independently. Hoist the fetch, pass the same spec object to every item.

  8. Keep spec and context JSON-serializable. <StudioComposition /> is a "use client" component, so in RSC both props cross the server to client boundary. Class instances, functions, Map/Set, and Date objects break serialization: pass plain objects, arrays, strings, numbers, booleans, null.

  9. 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.

  1. compositionUid: which composition to render (or the url / templateContentTypeUid that resolves it).
  2. dataShape: the object you hand to context, so we can check it matches the composition’s binding field names.
  3. renderPath: CSR or SSR. Picks step 5 vs step 6.
  4. 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.

  • mountFile renders <StudioComposition /> with context plus exactly one of spec / compositionUid.
  • In SSR the spec is resolved with sdk.fetchComposition on the server and passed as spec, no compositionUid on a server-rendered mount.
  • context field names match the bindings the composition was authored against.
  • A loop fetches the spec once above it and reuses the same spec for every item.
  • The compositionUid path passes both loadingFallback and errorFallback.
  • spec and context are 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

PitfallWhy it bitesFix
Neither spec nor compositionUid passedThe component throws StudioComposition requires either a 'spec' or a 'compositionUid'.Pass one: spec for SSR, compositionUid for client-only
compositionUid on a server-rendered mountThe fetch is a client effect. The server renders nothing and content pops in latePre-fetch with sdk.fetchComposition, pass spec
Passing data={...} instead of context={...}data is the <StudioComponent /> prop. <StudioComposition /> ignores it and renders with no template dataUse context, see understand-bring-your-own-data § Why context is not data
context shape doesn’t match the bindingsBound nodes render blank with no errorPass the element in the shape the composition was authored against (same field names)
Expecting spec.data to be populatedfetchComposition skips the data fetch by designSupply the data through context
sdk.public.fetchComposition(...)studioSdk.init(...) already returns the public surfaceCall sdk.fetchComposition(...)
Spec fetched inside the loop bodyOne network call per itemHoist the fetch above the loop
No loadingFallback / errorFallbackBoth default to null: a failed or slow fetch renders an empty gapPass both on the compositionUid path
Non-serializable context in RSCNext.js rejects the prop crossing the client boundaryMap to a plain object before passing
compositionUid path with no studioSdk.initConsole error SDK not initialized, then errorFallbackInitialize the SDK at app startup (install-studio)
Composition renders unstyledDesign tokens come from spec.config via the composition itselfConfirm the composition resolves (a real spec), and that breakpoints/tokens are registered (import-design-tokens)

See Also