Composition Rendering Reference

View as Markdown
Last updated July 17, 2026

<StudioComponent />

Source-grounded reference for what <StudioComponent /> accepts and what flows into the underlying SDK fetchers. Every type below maps to a real export in composable-studio-sdk; line references are to the files in composable-studio-sdk.

The Two Props

<StudioComponent /> accepts exactly two props (studio-react/src/renderer/renderer.type.ts:46):

interface StudioComponentProps {
  specOptions: StudioComponentSpecOptions;   // required
  data?: ComposableStudioData["component_props"];  // optional
}

specOptions: the resolved composition

Type definition (studio-client/src/composable-studio/composable-studio.type.ts:265):

type StudioComponentSpecOptions = {
  spec: StudioSpec | null;          // resolved composition; null when 404
  fetchOptions: StudioComponentFetchOptions;  // what was used to fetch
  hasSpec: boolean;                 // true when a composition was found
  hasTemplate: boolean;             // true when the composition has a connected template
  seo: SeoMetadata | null;          // SEO metadata for the <head>
};

You don't hand-construct this object. It comes from the SDK fetchers:

  • SSR: await sdk.fetchCompositionData(query, options?)
  • CSR: useCompositionData(query, options?) returns { specOptions, isLoading, error, refetchSpec, refetchData }

data: runtime external values

Type ComposableStudioData["component_props"] is structurally a Record<string, any>. Authors see your data object's top-level keys under the Component Default Data root in the canvas Data Picker, where they can bind component props to them.

Use this for anything not in Contentstack, such as pricing APIs, A/B variant flags, geolocation, weather, user profile, and URL search params. See Component Default Data and the wire-external-data skill.

<StudioComponent
  specOptions={specOptions}
  data={{ pricing: livePricingData, persona: visitorPersona, abVariant: "B" }}
/>

SDK Init Config (Set Once at App Shell)

Before any fetcher runs, you initialise the SDK with studioSdk.init({...}). The config schema (studio-client/src/config/config.schema.ts):

FieldTypePurpose
stackSdkStack \| anyRequired. The @contentstack/delivery-sdk stack instance. Must have livePreviewQuery available.
contentTypeUidstring (optional)The content type in your stack that STORES compositions, typically named compositions, documentation_compositions, or similar. One per project. This is NOT the connected content type the template renders against; that's templateContentTypeUid on the query (see below).
localestring (optional)Default locale for all fetches. Per-query locale in CompositionQueryOptions overrides this.
cslp.appendTagsboolean (optional)Append data-cslp Live Preview tags to bound DOM elements.
openInStudioButton.enableboolean (optional)Render the "Open in Studio" floating button on visitor pages (editor convenience).
openInStudioButton.position"top" \| "top-left" \| "top-right" \| "top-center" \| "bottom" \| "bottom-left" \| "bottom-right" \| "bottom-center" (optional)Where to position the button.
fetchCompositionCompositionFetcher (optional)Global custom fetcher for compositions. Per-query fetchComposition (below) takes priority over this.
fetchTemplateEntryTemplateEntryFetcher (optional)Global custom fetcher for template entries. Per-query fetchTemplateEntry (below) takes priority.
// src/lib/contentstack.ts (or similar app-shell module)
import { studioSdk } from "@contentstack/studio-react";
import { stack } from "./delivery-sdk";

studioSdk.init({
  stackSdk: stack,
  contentTypeUid: "documentation_compositions",   // where YOUR compositions are stored
  locale: "en-us",
  cslp: { appendTags: true },
});

Two different contentTypeUid concepts: don't confuse them

What it identifiesWhere you set it
contentTypeUid (config)The content type in your stack where compositions themselves are stored (one per project).studioSdk.init({ contentTypeUid: "documentation_compositions" })
templateContentTypeUid (query)The content type the composition's template is connected to (Blog Post, Product, etc.).fetchCompositionData({ templateContentTypeUid: "product" })

The config-level one is set once, at app shell. The query-level one is set per route, per fetch, and only when you're identifying a composition by its connected CT instead of by composition UID or URL.


What You Pass to the Fetcher (Becomes specOptions)

sdk.fetchCompositionData(query, options?) and useCompositionData(query, options?) take two arguments. Both fetchers return / resolve to the same specOptions object.

query: CompositionQueryInput: identifies which composition

Type (studio-messenger/src/fetch-spec.type.ts:8). One of these five shapes; the SDK enforces at-least-one-identifier via the type:

ShapeWhen to use
{ compositionUid: string }You know the composition's UID (fastest path). Pair with templateEntryUid (in options) to also pin a specific entry; otherwise the first entry of the connected CT is used.
{ url: string }Resolve by URL pattern matching, slower (scans compositions).
{ compositionUid: string, url: string }Both: UID resolves the composition, URL preserved for context.
{ templateContentTypeUid: string }Pick a composition by its connected content type: e.g. templateContentTypeUid: "product" returns the first composition connected to the product CT. Pair with templateEntryUid to pin a specific entry.
{ url: string, templateContentTypeUid: string }URL + CT filter; CT takes priority for the fetch, URL preserved for Studio iframe context.

The templateContentTypeUid field is how you pass the connected content type of a template through to specOptions. It's the CT the template renders entries of (Blog Post, Product, Case Study, etc.), not the CT that stores compositions in your stack. See Two different contentTypeUid concepts below.

options?: CompositionQueryOptions: modifiers on the fetch

Type (studio-client/src/composable-studio/composable-studio.type.ts:128):

OptionTypePurpose
variantAliasstringRender a specific variant alias of the entry. See Variant aliases deep-dive.
localestringOverride the stack default locale for THIS query. Studio's native fallback chain still applies.
templateEntryUidstringUse a specific entry UID as the preview/template entry. Priority order: templateEntryUid > url resolution > first available entry.
extendQueryRecord<string, { includeReferences?: string[]; only?: (string \| [string, string])[] }>Extend the underlying CDA query with extra reference inclusions or field projections, keyed by content type UID.
fetchCompositionCompositionFetcherPer-query override of how the composition itself is fetched. Receives a context with compositionQuery, contentTypeUid, options, stackSdk, and defaultFetch. Return a CompositionEntry, null for 404, or call defaultFetch() to fall through.
fetchTemplateEntryTemplateEntryFetcherPer-query override of how the template entry is fetched. Same shape; gets the resolved composition + computed includeReferences and only projections.

The fetcher overrides (fetchComposition, fetchTemplateEntry) are the escape hatch for custom storage backends, multi-region fanout, mocked tests, etc.


End-to-End Example

// app/products/[sku]/page.tsx — Next App Router, SSR
import { sdk } from "@/lib/contentstack";
import { StudioComponent } from "@contentstack/studio-react";

export default async function ProductPage({ params, searchParams }) {
  const variant = await pickExperimentVariant(params.sku);
  const locale = searchParams.locale ?? "en-us";

  // Fetch — resolves the composition + bindings, returns the specOptions object
  const specOptions = await sdk.fetchCompositionData(
    { templateContentTypeUid: "product" },        // CompositionQueryInput — identifies WHICH composition
    {                                              // CompositionQueryOptions — modifiers
      templateEntryUid: params.sku,
      variantAlias: variant === "B" ? "experiment-b" : undefined,
      locale,
      extendQuery: {
        product: { includeReferences: ["related_products", "reviews"] },
      },
    },
  );

  // External runtime data — anything not in Contentstack
  const pricing = await fetchLivePricing(params.sku);
  const persona = getPersonaFromCookies();

  // Hand the resolved object + runtime data to StudioComponent
  return (
    <StudioComponent
      specOptions={specOptions}
      data={{ pricing, persona }}
    />
  );
}

That's the full surface. Two props on <StudioComponent />; a CompositionQuery + CompositionQueryOptions on the fetcher. Everything else is either internal to the SDK or surface a different SDK API touches (e.g. studioSdk.init({...}) config).


Common Confusions

  • specOptions is NOT { compositionUid, templateEntryUid }: that's the query/options input to the fetcher, not what <StudioComponent /> consumes. Always go through fetchCompositionData / useCompositionData first; pass the resolved result to <StudioComponent />.
  • data is NOT a way to override Contentstack content. Components inside the composition only see data values that authors bind to via the Data Picker. Authors decide which data keys flow into which component props.
  • 404 handling lives on specOptions.hasSpec and specOptions.hasTemplate: check these before rendering to short-circuit to your 404 component when no composition / no connected entry was found.
  • Editor-only variant override is handled by Studio + the SDK, not by your code. When an editor picks a variant in the canvas navbar, Studio applies it to the iframe preview internally; your visitor-site code only ever passes the variantAlias query option (above). Do not parse Studio's iframe URL params; they're an internal contract. See Variant aliases deep-dive.

See Also