Skip to main content

Radix Umbrella Slot Root Crash

This troubleshooting note covers the dependency migration in 9a51be0, the partial type-only follow-up in 76d14ce, and the runtime fix in 7a20d6d.

Symptoms

  • The app crashes with a React runtime error such as Element type is invalid.
  • The failure appears only on shadcn-style asChild paths, especially in sidebar menu components.
  • TypeScript may still pass if the code only adds a cast around the component variable.

Diagnosis

Compare the current owned-forked primitives:

  • lumie-frontend/components/ui/button.tsx uses const Comp = asChild ? Slot.Root : "button".
  • lumie-frontend/components/ui/sidebar.tsx now does the same for SidebarGroupLabel, SidebarGroupAction, SidebarMenuButton, SidebarMenuAction, and SidebarMenuSubButton.

That is the key clue. With the radix-ui umbrella package, Slot is a namespace object, not the directly renderable component value that older @radix-ui/react-slot call sites expected.

Root Cause

9a51be0 migrated shadcn primitives from package-specific imports such as @radix-ui/react-slot to the umbrella import import { Slot } from "radix-ui".

Some call sites still rendered bare Slot in asChild ? Slot : ... expressions. That compiled, but at runtime React received an object instead of a component and threw the invalid element-type error.

76d14ce added React.ElementType casts in sidebar.tsx, which fixed the typing problem but not the runtime behavior. The actual fix landed in 7a20d6d, which changed the render targets to Slot.Root.

Fix

When importing from radix-ui, render Slot.Root rather than bare Slot in asChild branches.

Use lumie-frontend/components/ui/button.tsx as the reference pattern. The sidebar components now match it.

Prevention And Verification

  • After any Radix dependency migration, grep for asChild ? Slot : or other bare Slot render expressions inside components/ui.
  • Treat React.ElementType casts as type adapters only; they do not make an invalid runtime component valid.
  • Verify current source:
    • lumie-frontend/components/ui/button.tsx uses Slot.Root.
    • lumie-frontend/components/ui/sidebar.tsx uses Slot.Root for every asChild branch that previously crashed.

Operational Detail

This is a dependency-shape failure. The TypeScript surface can look acceptable while React still receives a value it cannot render.

When asChild is involved, the failing component usually does not crash at import time. It crashes only when a consumer actually renders the asChild branch, which is why sidebar, dropdown, and button composition paths are the highest-risk areas.

SignalMeaning
Element type is invalidReact received an object or undefined instead of a component.
Crash only with asChildThe normal element branch is fine; the slot branch is wrong.
Cast to React.ElementType compilesThe type system was bypassed, but runtime shape was not fixed.

Recovery Playbook

  1. Search owned UI primitives for bare Slot render targets.
  2. Replace Slot with Slot.Root where the import comes from radix-ui.
  3. Remove casts that were only hiding the mismatch unless another generic type issue remains.
  4. Render the affected components in their asChild mode, not only their default mode.
  5. Check routes that compose sidebar links, menu buttons, dropdown triggers, and command items.

Verification Evidence

Collect evidence at two levels:

  • static: rg -n "asChild \\? Slot(\\s|:)" components src app returns no bare umbrella Slot render branches,
  • runtime: a page using the affected asChild components renders without the invalid element-type error,
  • regression: the normal non-asChild branch still renders the expected native element.

Safe Future Changes

For future Radix upgrades, inspect import semantics before running broad codemods. Package-specific imports and umbrella imports can expose the same conceptual primitive with a different runtime shape. Migration review should include at least one composed shadcn component path, because those paths exercise the slot primitive more heavily than basic button rendering.