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
| Pitfall | Why it bites | Fix |
|---|---|---|
| Using default: instead of defaultValue: | Studio silently ignores default:, palette preview renders blank | Use defaultValue: exactly |
| registerComponent without thumbnailUrl | Registry 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 anything | Add 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 URL | The 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 Router | Next 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 one | Import 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 list | The 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 error | Use 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 types | These don’t exist. Registration is rejected at runtime | Use the 13 allowed types only |
| Hardcoding an interactive component’s behavior instead of exposing an action prop | Authors 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 app | Declare type: "action" for the callback and register the function it can call. See wire-studio-state |
| A design prop typed string instead of choice | A 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 value | Type 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 rejected | Map 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 component | The Data Picker offers a binding that crashes at render time | Read the source. Register only props you see |
| Duplicate type: UID across two components | First 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 startup | Components never appear in the palette | Add import "./register-components" to main.tsx / _app.tsx |
| Registering the unwrapped primitive (e.g. Box from a UI lib) instead of the customer’s component | Loses brand styling. Studio composes pages out of unstyled primitives | Register 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 literals | Authors can’t type freeform text into a free-form field | Use 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 do | Eager 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 prop | Slots 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 render | The 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 preview | Bindings 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 canvas | The 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. |