Studio Docs

When to Use

The pitfall table for component registration: what fails silently, why, and the fix.

Part of the component-registration set. Start at register-component, which routes here.

Common Pitfalls

PitfallWhy it bitesFix
Using default: instead of defaultValue:Studio silently ignores default:, palette preview renders blankUse defaultValue: exactly
registerComponent without thumbnailUrlRegistry defaults it to "", so Studio renders a text placeholder, not a preview. No error, no warning. A twenty-component palette becomes an unbrowsable text wall and authors can’t find anythingAdd a thumbnailUrl inline-SVG data URI to every registration. Sweep the file: registerComponent count must equal thumbnailUrl count
thumbnailUrl pointing at a CDN / Contentstack asset / /public URLThe palette renders inside Studio’s iframe. External requests hit CORS or fail silently, so the tile is blank or broken-image. Looks identical to “no thumbnail set”Inline SVG data URI via encodeURIComponent. Zero network fetches. See palette-conventions
Registering components from a Server Component graph on Next App RouterNext throws “Attempted to call the default export … from the server but it’s on the client”, or the palette is empty because the server-side registry got populated instead of the client oneImport the registration module from the "use client" boundary that runs studioSdk.init (app/studio-init.tsx), never from layout.tsx or a server page.tsx. See step 5b
type: "any" on a prop bound to a Group / Global Field / block object, or to a listThe binder recursively unwraps single-key objects for every type except object/array, so a single-field block collapses to its inner value. Lookups return undefined, the array’s indices show as {0,1,2,3}, and the component renders blank with no errorUse object for a group/block, array for a list, the only two types that skip the unwrap. Reserve any for genuine primitives. See § The any flatten trap
Using text, link, color, image as prop typesThese don’t exist. Registration is rejected at runtimeUse the 13 allowed types only
Hardcoding an interactive component’s behavior instead of exposing an action propAuthors can drop the component but can’t decide what it does. Every new behavior needs a code change. The palette looks complete while Studio only decorates the appDeclare type: "action" for the callback and register the function it can call. See wire-studio-state
A design prop typed string instead of choiceA free string re-opens the drift the library exists to prevent: color: '#6c5ce7', gap: '24px', one value per author. choice with a closed options list means the author picks from a menu and cannot type a raw valueType every design prop (color, size, align, variant, gap, tag) as choice with options + defaultValue. Reserve string for content
Copying a prop type off a built-in node into a registerComponent schema (e.g. plaintext from the built-in Header)Built-ins use a separate prop-type vocabulary. plaintext isn’t one of the 12 and the registration is rejectedMap to the registered-component equivalent (plaintext becomes string). Read built-in defs for structure, never for prop-type strings
Registering a prop that’s not actually on the componentThe Data Picker offers a binding that crashes at render timeRead the source. Register only props you see
Duplicate type: UID across two componentsFirst registration wins. Duplicates are silently skipped (the SDK’s default is idempotent so HMR / RSC-plus-client / build-worker re-evaluation doesn’t break). A real bug stays hidden because the second definition never lands.Search the registration file for the UID before writing. Pass { strict: true } as the second arg to registerComponents([...], { strict: true }) to throw on duplicates instead.
Forgetting to import the registration file at startupComponents never appear in the paletteAdd import "./register-components" to main.tsx / _app.tsx
Registering the unwrapped primitive (e.g. Box from a UI lib) instead of the customer’s componentLoses brand styling. Studio composes pages out of unstyled primitivesRegister the customer’s wrapped component (e.g. <Hero>), not the lib primitive
Inferring a string prop as choice because TS uses a union of two literalsAuthors can’t type freeform text into a free-form fieldUse choice only when the union is a closed set the user MUST pick from
Arity-0 component (function Header() {…} as component: Header)SDK treats as lazy loader and calls it outside render, which throws React #321 “Invalid hook call”.Use lazy shape () => import(...) or give it a props param.
Registering eagerly when lazy would doEager ships in entry bundle. Many components balloon TTI.Default to lazy () => import("./Foo").
Component depends on layout ancestor (e.g. .fs-card only sized by .fs-grid)Renders full-bleed in a Slot/Repeater that doesn’t provide that ancestor.Decouple. See Layout contract above. Layout lives in parent Section. See use-section-slot § Layout container.
Setting defaultValue on a slot propSlots start empty until authors fill them. A static default doesn’t fit the model and is silently ignored.Omit defaultValue on slot props. The slot renders as a dashed drop target until an author drops content.
Naming a slot prop something the component doesn’t renderThe slot becomes a drop target in Studio but its contents never appear at render time because the component ignores the prop.The prop name must match what the React component’s signature uses, typically children (for <Card>{...}</Card> style), but any name works as long as the component renders {propName} somewhere in its JSX.
Trying to constrain a slot to “only accept Sections of type X”Slots accept any registered component or Section. No built-in type filter exists. Adding a constraint isn’t supported.If you need a typed value override (one specific kind of content), use an exposed Section Prop instead. If you need a typed component constraint, surface the expectation in the slot’s displayName (“Drop a CTA Section here”) so authors self-route.
Component throws on undefined / null / empty-array props during canvas previewBindings resolve at runtime. An unfilled entry field, empty multi-file field, or unbound prop surfaces undefined to the component. One thrown access kills the whole canvas render. Author can’t recover without re-binding.See § Null-safe rendering contract above. Optional-chain every nested access (props.image?.url). Return null or a skeleton when essential data is missing.
Confusing slot (component prop type) with Section Slot (drop region inside a Section)They are different mechanisms at different levels, both surface drop targets, both can hold the other.A slot PROP is part of a registered component’s schema. A Section Slot is a Smart Container carved into a Section. The recommended composition runs in three steps: declare a slot prop on the component, carve a Section Slot inside that prop via use-section-slot § component-slot-prop placement, then let authors fill that Section Slot at template time.
Design tab is empty for this component when selected in the canvasThe registration didn’t declare a styles block. The Design tab surfaces the sections listed in styles, so with no styles the tab is empty.Add styles: [...] to the registration listing the categories authors should be able to edit (size, spacing, typography, background, etc.). See docs/bring-your-own-components/component-schema-prop-types.md § styles. If the Design tab itself is missing (not just empty), the project’s Freeform Feature is off (see install-studio § After install) if the Design tab is missing / disabled.