---
name: register-component-pitfalls
---

## 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`](register-component.md), 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`](wire-studio-state.md) |
| 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/15-build/10-components/25-component-schema-props-types-and-defaults.md`](https://studio-documentation.contentstackapps.com/components/component-schema-props-types-and-defaults) § styles. If the Design tab itself is missing (not just empty), the project's Freeform Feature is off (see [`install-studio`](install-studio.md) § After install) if the Design tab is missing / disabled. |
