Studio Docs

StudioComposition

<StudioComposition /> renders a composition (a full page, or a section as the root) against data you already hold, passed in as a context prop. No SDK data fetch, no hook. It works in CSR and Next.js SSR.

Use it when the data isn’t coming from the SDK’s normal template fetch: you fetched it yourself, it’s one element of an array you’re looping, or it comes from a system other than Contentstack.

Note: This is different from <StudioComponent />. <StudioComponent /> fetches a composition for a route and renders it with its bound CMS data. <StudioComposition /> renders a composition you hand the data to. See Two SDK components for the <StudioComponent /> surface.

Do It With a Skill

render-with-own-data wires <StudioComposition context={...}> against data you already hold, using the spec-only sdk.fetchComposition.

curl -fsSL https://studio-documentation.contentstackapps.com/install.sh | sh

What It Does

You give it two things: which composition to render, and the context data to render it against.

  • Which composition: either a pre-fetched spec (from sdk.fetchComposition) or a compositionUid it fetches for you on the client.
  • The data: a context object, placed internally at dataSources.template. The composition’s template bindings resolve against it. Pass the raw element you already hold (for example one item of a multiple-group field). The SDK wraps it, you don’t build a full spec-data envelope.

Embedded sections inside the composition are auto-scoped from your context (no network). You never build section_scoped_data yourself.

The Props

interface StudioCompositionProps {
  context: unknown;                    // required — the data to render against
  spec?: StudioSpec;                   // a pre-fetched, spec-only composition
  compositionUid?: string;             // OR a uid the component fetches on the client
  loadingFallback?: React.ReactNode;   // shown during the compositionUid fetch (default null)
  errorFallback?: React.ReactNode;     // shown if the compositionUid fetch fails (default null)
}

You must pass either spec or compositionUid: the component throws if neither is present. When spec is provided, compositionUid is ignored and there is no loading phase (nothing to fetch).

For the full type-level reference, see <StudioComposition /> reference.

Two Ways to Give It a Composition

1. Pre-fetch the spec (works in SSR and CSR)

Resolve the composition once with sdk.fetchComposition, then pass the returned spec. This is the only path that works during a server render: a "use client" component can’t fetch on the server.

import { sdk } from "@/lib/contentstack";
import { StudioComposition } from "@contentstack/studio-react";

// Spec-only fetch — resolves the composition STRUCTURE, skips the data fetch.
const spec = await sdk.fetchComposition({ compositionUid: "hero_section", searchQuery: "" });

// `element` is data you already hold.
<StudioComposition spec={spec} context={element} />;

sdk.fetchComposition(query, options?) returns a Promise<StudioSpec> whose data is intentionally empty. This is the bring-your-own-data path. The query takes the same identifier shapes as sdk.fetchCompositionData (compositionUid, url, templateContentTypeUid) plus the searchQuery string.

2. Pass a compositionUid (client only)

Hand the component a compositionUid and it fetches on the client. Give it loadingFallback and errorFallback for the fetch phase.

"use client";
import { StudioComposition } from "@contentstack/studio-react";

<StudioComposition
  compositionUid="hero_section"
  context={element}
  loadingFallback={<SectionSkeleton />}
  errorFallback={<NotFound />}
/>;

A failed fetch is logged to the console and renders errorFallback. While the fetch is in flight, loadingFallback renders.

Tip: In SSR, prefer the pre-fetched spec path. You can resolve the spec on the server and stream markup immediately, with no client-side loading state. Reserve the compositionUid path for client-rendered surfaces.

Rendering a Section Per Array Element (the Loop)

The common case: you hold a multiple-group array and want to render the same section once per element, each against its own data. Fetch the spec once, map over your data.

import { sdk } from "@/lib/contentstack";
import { StudioComposition } from "@contentstack/studio-react";

const spec = await sdk.fetchComposition({ compositionUid: "product_tile", searchQuery: "" });

export function ProductGrid({ products }: { products: ProductElement[] }) {
  return (
    <div className="grid">
      {products.map((element, i) => (
        <StudioComposition key={i} spec={spec} context={element} />
      ))}
    </div>
  );
}

Fetching the spec once and reusing it across the loop avoids a fetch per item. If you use the compositionUid path inside a loop instead, each <StudioComposition /> fetches independently, fine for a handful, wasteful for many.

Rendered, that’s one section three times over, each against its own element, no CMS entry behind any of them:

Browser page titled “Latest releases” with three cards side by side: Studio 1.4 Bring your own data, Studio 1.3 Slot defaults, Studio 1.2 Smart containers. Each card is the same Featured Card section composition rendered against a different element of an array the page holds itself, with its title, body and “Read more” link resolving from that element.

The section itself is unchanged, the same one an author built against a content type. Only where its template data comes from differs.

Works in Both CSR and SSR

context is just a React prop, so the component renders wherever React runs. Only the fetch path for the composition differs.

CSR example: Vite / React SPA

"use client";
import { useEffect, useState } from "react";
import { StudioComposition, type StudioSpec } from "@contentstack/studio-react";
import { sdk } from "@/lib/contentstack";

export function HeroBlock({ element }: { element: unknown }) {
  const [spec, setSpec] = useState<StudioSpec | null>(null);

  useEffect(() => {
    sdk.fetchComposition({ compositionUid: "hero_section", searchQuery: "" }).then(setSpec);
  }, []);

  if (!spec) return <SectionSkeleton />;
  return <StudioComposition spec={spec} context={element} />;
}

Or skip the prefetch entirely and let the component fetch:

"use client";
import { StudioComposition } from "@contentstack/studio-react";

export function HeroBlock({ element }: { element: unknown }) {
  return (
    <StudioComposition
      compositionUid="hero_section"
      context={element}
      loadingFallback={<SectionSkeleton />}
      errorFallback={<NotFound />}
    />
  );
}

SSR / RSC example: Next.js App Router

Resolve the spec on the server, render immediately, no client loading state.

// app/products/page.tsx — Server Component (no "use client")
import { sdk } from "@/lib/contentstack";
import { StudioComposition } from "@contentstack/studio-react";

export default async function ProductsPage() {
  // Your own data fetch + the spec-only composition fetch, in parallel.
  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>
  );
}

Equivalent patterns for other frameworks

FrameworkWhere to fetch the specWhere to fetch your dataWhere <StudioComposition /> mounts
Vite / React SPAuseEffect + sdk.fetchComposition, or the compositionUid pathuseEffect / React Query / SWRDirectly in the page component
Next.js Pages RoutergetServerSideProps calls sdk.fetchCompositionSame getServerSidePropsPage component receives both as props
Next.js App Router (RSC)Server Component awaits sdk.fetchCompositionServer Component awaits your fetchRendered in the Server Component (or a "use client" child)
Remixloader calls sdk.fetchCompositionSame loaderRoute component reads via useLoaderData
Astro (with React island).astro server-side fetchSame .astro server-side fetchReact island marked client:load

For the full render-strategy decision, see CSR vs SSR.

How context maps to bindings

The composition an author built has bindings against its template. When you render with <StudioComposition />, your context object becomes that template data, so a binding to title reads context.title, a binding to sub_items reads context.sub_items, and so on.

Pass the element in the same shape the composition’s bindings expect. If the composition was authored against a product template, context is one product-shaped object. If it iterates sub_items, context has a sub_items array.

Common Pitfalls

PitfallSymptomFix
Neither spec nor compositionUid passedComponent throws at renderPass one of them: spec for SSR, compositionUid for client-only
Using compositionUid during a server renderNothing renders on the server. Content pops in on the clientPre-fetch with sdk.fetchComposition and pass spec instead
context shape doesn’t match the composition’s bindingsBound nodes render blankPass the element in the shape the composition was authored against (same field names)
compositionUid path with no fallbacksBlank flash during fetch / silent failurePass loadingFallback and errorFallback
Re-fetching the spec inside a loopExtra network calls, slow renderFetch the spec once above the loop, pass the same spec to each item
Expecting spec.data to be populatedspec.data comes back emptyThat’s by design: fetchComposition skips the data fetch, you provide it via context

See Also