Rendering
Your product page has a promo bar and a product hero with an interactive "Add to cart" button. Each is a block function that returns something, but what actually reaches the browser, and does the cart button still work there?
This page shows the two ways a block becomes UI, how to pick one, and the rules that hold in both.
- Descriptors (data mode): the block returns plain JSON such as
{ component: "product-hero", props: {…} }, and a view registry in your app (code that maps eachcomponentname to a React component) renders it on the server and again in the browser. - React Server Components (RSC): the block returns JSX that renders only on the server. React sends the result to the browser in its own wire format, called Flight, and only components marked
"use client"ship JavaScript.
Deco CMS returns whatever your block function returns, so it works with both. Each framework guide uses one:
| Guide | Block returns | Streaming | Ships to the browser |
|---|---|---|---|
| Next.js | JSX | Server Component Suspense boundaries | Client Components |
| TanStack Start | Serializable descriptors | Deferred promises with Await | The view registry and its components |
| TanStack Start + RSC | JSX | RSC stream (Flight) | Client references and the RSC runtime |
You already render the banner as a plain component: <PromoBanner title="Free shipping over $50" />. The same block in both modes:
// RSC (Next.js, TanStack Start + RSC): the block returns a ready element
"promo-banner": (input: PromoBannerProps) => <PromoBanner {...input} />,
// Data mode (TanStack Start): the block returns a descriptor…
"promo-banner": (input: PromoBannerProps) => ({ component: "promo-banner", props: input }),
// …and your view registry renders it
const views = { "promo-banner": PromoBanner };Which mode to use
Use descriptors when views must render in an ordinary browser React app. Use RSC when your framework supports it and you want server-only view code to stay off the client. RSC doesn't guarantee smaller responses, so measure payload size, hydration and navigation on real pages.
Block functions run on the server
In all three guides, block functions run on the server, so data fetching belongs there. In data mode, a block returns a descriptor and a synchronous React view renders its props. With RSC, a block returns JSX that can include Client Components.
Don't put a Client Component (a component from a "use client" file) in the block map directly. On the server, importing it gives you a placeholder React can render but your code can't call, and Deco CMS calls every block function. Wrap it instead: cart: (input: CartProps) => <CartButton {...input} />. Block functions should only read; see The rule in full.
Errors, streaming, and cancellation
- Start every block first. Await critical data, such as SEO, only after all of the page's blocks have started, so they fetch in parallel instead of waiting behind the SEO call.
- One Suspense boundary per block lets each block appear as soon as it's ready.
- Suspense handles loading, not errors. Each
client.resolve(block)returns[value, error]; render a fallback whenerroris set. Errors thrown later while React renders need an error boundary. - A hidden block is not an error. A block an editor hid resolves to
undefined, with no error: render nothing for it. Blocks never renders anything itself, so what to show is always your app's choice. - Cancellation is your app's job. Deco CMS never sees the request. A block that fetches upstream should take the request's
signalfrom wherever your app keeps request state. - Once streaming starts, the status code is fixed. The status line and headers are already sent, so a later error can't change them. Decide 404s and redirects (the
matchRouteresult) before you stream.