spree/spree · error · Error

useSidebar must be used within a SidebarProvider.

Error message

useSidebar must be used within a SidebarProvider.

What it means

useSidebar() reads SidebarContext, created only by <SidebarProvider> in @spree/dashboard-ui. The provider owns desktop open state, mobile open state, and isMobile detection; every sidebar compound component (trigger, menu, inset) consumes it. Calling the hook outside the provider throws.

Source

Thrown at packages/dashboard-ui/src/ui/sidebar.tsx:45

  /**
   * Sets the state without remembering it. For collapses the app imposes on
   * the user's behalf (e.g. the settings area folding the nav to icons to make
   * room for its own): the merchant's own choice, made through the trigger,
   * must survive it.
   */
  setOpenTransient: (open: boolean) => void
  openMobile: boolean
  setOpenMobile: (open: boolean) => void
  isMobile: boolean
  toggleSidebar: () => void
}

const SidebarContext = React.createContext<SidebarContextProps | null>(null)

function useSidebar() {
  const context = React.useContext(SidebarContext)
  if (!context) {
    throw new Error('useSidebar must be used within a SidebarProvider.')
  }

  return context
}

function SidebarProvider({
  defaultOpen = true,
  open: openProp,
  onOpenChange: setOpenProp,
  className,
  style,
  children,
  ...props
}: React.ComponentProps<'div'> & {
  defaultOpen?: boolean
  open?: boolean
  onOpenChange?: (open: boolean) => void
}) {

View on GitHub (pinned to 06bf66a868)

Solutions

  1. Wrap the app layout (the part containing the sidebar and its trigger) in <SidebarProvider>
  2. In tests, render(<SidebarTrigger />, { wrapper: SidebarProvider })
  3. Keep the trigger inside the provider tree — if the header lives outside, lift the provider up to their common parent
  4. For pages without a sidebar that still need a collapse button, drive your own state instead of useSidebar()

Example fix

// before
<header><SidebarTrigger /></header> {/* outside the provider */}
<SidebarProvider>
  <Sidebar>...</Sidebar>
</SidebarProvider>

// after — provider wraps both sidebar and trigger
<SidebarProvider>
  <AppShell>
    <Sidebar>...</Sidebar>
    <header><SidebarTrigger /></header>
  </AppShell>
</SidebarProvider>
Defensive patterns

Strategy: validation

Validate before calling

// SidebarProvider must wrap both <Sidebar> and every <SidebarTrigger> — put it
// around the whole app layout:
<SidebarProvider>
  <AppShell>{/* Sidebar + header with SidebarTrigger */}</AppShell>
</SidebarProvider>

Prevention

When it happens

Trigger: Rendering <SidebarTrigger> (or any Sidebar* compound component / custom component calling useSidebar()) outside <SidebarProvider> — e.g. a collapse button placed in a header rendered before the provider, a sidebar widget reused on a non-sidebar page, or a test render without the wrapper.

Common situations: Shadcn-style sidebar adoption in a custom host app: developers copy <Sidebar> markup but skip the provider; unit tests for menu components; refactoring the app shell so the sidebar renders on a route that no longer sits under the provider.

Related errors


AI-assisted analysis of spree/spree@06bf66a868 (2026-08-21). Data as JSON: /api/errors/c9895575a687f5b8. Report an issue: GitHub.