ui icon indicating copy to clipboard operation
ui copied to clipboard

[bug]: (sidebar-13): Function components cannot be given refs when using Sidebar in a Dialog

Open suprunchuk opened this issue 9 months ago • 8 comments

Describe the bug

When using the Sidebar component within a Dialog component from shadcn/ui, a warning is thrown in the console:

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Image

Affected component/components

SidebarMenuButton

How to reproduce

  1. Set up a new project with shadcn/ui components installed.
  2. Create a settings-dialog.tsx file with the provided code that nests a Sidebar inside a Dialog.
  3. Create a page.tsx file in the app/dashboard directory with the provided code to render the SettingsDialog.
  4. Run the application using npm run dev or equivalent.
  5. Open the browser console and trigger the SettingsDialog by clicking the "Open Dialog" button.
  6. Observe the warning: Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Codesandbox/StackBlitz link

https://ui.shadcn.com/blocks/sidebar#sidebar-13

Logs

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Check the render method of `SlotClone`.
SidebarMenuButton@http://localhost:5173/src/components/ui/sidebar.tsx:497:27
SlotClone<@http://localhost:5173/node_modules/.vite/deps/chunk-HDHGXYFG.js?v=88a60195:73:38
Slot<@http://localhost:5173/node_modules/.vite/deps/chunk-HDHGXYFG.js?v=88a60195:54:38
Primitive</Node<@http://localhost:5173/node_modules/.vite/deps/chunk-L4XL26MT.js?v=88a60195:41:44
DialogTrigger<@http://localhost:5173/node_modules/.vite/deps/@radix-ui_react-dialog.js?v=88a60195:1156:48
DialogTrigger@http://localhost:5173/src/components/ui/dialog.tsx:25:23
li
SidebarMenuItem@http://localhost:5173/src/components/ui/sidebar.tsx:467:25
ul
SidebarMenu@http://localhost:5173/src/components/ui/sidebar.tsx:454:21
div
SidebarFooter@http://localhost:5173/src/components/ui/sidebar.tsx:360:23
div
div
div
Sidebar@http://localhost:5173/src/components/ui/sidebar.tsx:138:17
Provider@http://localhost:5173/node_modules/.vite/deps/chunk-BVAFDEYC.js?v=88a60195:51:47
Dialog@http://localhost:5173/node_modules/.vite/deps/@radix-ui_react-dialog.js?v=88a60195:1127:7
Dialog@http://localhost:5173/src/components/ui/dialog.tsx:14:16
AppSidebar@http://localhost:5173/src/components/app-sidebar.tsx?t=1741244310649:19:27
div
Provider@http://localhost:5173/node_modules/.vite/deps/chunk-BVAFDEYC.js?v=88a60195:51:47
TooltipProvider@http://localhost:5173/node_modules/.vite/deps/@radix-ui_react-tooltip.js?v=88a60195:2278:7
TooltipProvider@http://localhost:5173/src/components/ui/tooltip.tsx:13:25
SidebarProvider@http://localhost:5173/src/components/ui/sidebar.tsx:40:25
App@http://localhost:5173/src/App.tsx?t=1741244310649:18:57

System Info

System: Windows 11 pro
Binaries: Node.js v22.14.0, npm 11.1.0
Browsers: Chrome 134.0.6998.36

`package.json`



{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@radix-ui/react-avatar": "^1.1.3",
    "@radix-ui/react-collapsible": "^1.1.3",
    "@radix-ui/react-dialog": "^1.1.6",
    "@radix-ui/react-dropdown-menu": "^2.1.6",
    "@radix-ui/react-separator": "^1.1.2",
    "@radix-ui/react-slot": "^1.1.2",
    "@radix-ui/react-tooltip": "^1.1.8",
    "@tailwindcss/vite": "^4.0.9",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "lucide-react": "^0.477.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "swr": "^2.3.2",
    "tailwind-merge": "^3.0.2",
    "tailwindcss": "^4.0.9",
    "tailwindcss-animate": "^1.0.7"
  },
  "devDependencies": {
    "@eslint/js": "^9.21.0",
    "@types/node": "^22.13.9",
    "@types/react": "^19.0.10",
    "@types/react-dom": "^19.0.4",
    "@vitejs/plugin-react-swc": "^3.8.0",
    "eslint": "^9.21.0",
    "eslint-plugin-react-hooks": "^5.1.0",
    "eslint-plugin-react-refresh": "^0.4.19",
    "globals": "^15.15.0",
    "typescript": "~5.7.2",
    "typescript-eslint": "^8.24.1",
    "vite": "^6.2.0"
  }
}

Before submitting

  • [x] I've made research efforts and searched the documentation
  • [x] I've searched for existing issues

suprunchuk avatar Mar 06 '25 07:03 suprunchuk

Can I work on this issue @suprunchuk

sarathkumarsasi avatar Mar 07 '25 10:03 sarathkumarsasi

Can I work on this issue @suprunchuk

yeap

suprunchuk avatar Mar 07 '25 11:03 suprunchuk

The Drawer component also has this problem.

xzedx avatar Mar 09 '25 16:03 xzedx

Same happens for Input components inside Form component

Paradox1st avatar Mar 11 '25 05:03 Paradox1st

@shadcn Hi, can you fix this misunderstanding as soon as possible??

suprunchuk avatar Mar 11 '25 10:03 suprunchuk

@suprunchuk Can you share the code or a codesandbox link? thank you

shadcn avatar Mar 12 '25 07:03 shadcn

@suprunchuk Can you share the code or a codesandbox link? thank you

I just took the sidebar-13 component and added it to the clean project with Vite

suprunchuk avatar Mar 12 '25 07:03 suprunchuk

When you use asChild with a Radix UI component like DropdownMenuTrigger, it:

  1. Doesn't render its own DOM element
  2. Clones the child element you provide (in this case, SidebarMenuButton)
  3. Spreads all necessary props to that child element

The problem occurs when the child component (SidebarMenuButton) doesn't properly handle these forwarded props or doesn't forward them to its own HTML elements.

Fix the SidebarMenuButton component: Make sure it properly forwards refs and event handlers.

function SidebarMenuButton(
  {
    asChild = false,
    isActive = false,
    variant = "default",
    size = "default",
    tooltip,
    className,
    ...props
  }: React.ComponentProps<"button"> & {
    asChild?: boolean;
    isActive?: boolean;
    tooltip?: string | React.ComponentProps<typeof TooltipContent>;
  } & VariantProps<typeof sidebarMenuButtonVariants>,
  ref: React.ForwardedRef<HTMLButtonElement>
) {
  const Comp = asChild ? Slot : "button";
  const { isMobile, state } = useSidebar();

  const button = (
    <Comp
      ref={ref} // Forward the ref
      data-slot="sidebar-menu-button"
      data-sidebar="menu-button"
      data-size={size}
      data-active={isActive}
      className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
      {...props}
    />
  );

  if (!tooltip) {
    return button;
  }

  if (typeof tooltip === "string") {
    tooltip = {
      children: tooltip,
    };
  }

  return (
    <Tooltip>
      <TooltipTrigger asChild>{button}</TooltipTrigger>
      <TooltipContent
        side="right"
        align="center"
        hidden={state !== "collapsed" || isMobile}
        {...tooltip}
      />
    </Tooltip>
  );
}
// Forward ref using React.forwardRef after function definition
const ForwardedSidebarMenuButton = React.forwardRef(SidebarMenuButton);
ForwardedSidebarMenuButton.displayName = "SidebarMenuButton";
//Export
export {  ForwardedSidebarMenuButton as SidebarMenuButton } ;

thanhttb avatar Mar 13 '25 08:03 thanhttb

Solved by upgrading to React 19. In React 19, you can pass ref as a prop.

UdaraWanasinghe avatar Mar 31 '25 04:03 UdaraWanasinghe

Solved by upgrading to React 19. In React 19, you can pass ref as a prop.

works for me

zemelLeong avatar Apr 04 '25 10:04 zemelLeong

Same happens for Input components inside Form component

True! , based @thanhttb solution, and other info I gathered on React.forward, I created a copy of the Shadcn Input component, wrapped it in a React.forward function, and then used this new component in my form.

constant MyFormComponent = () => {
..........
**const InputWithRefF = React.forwardRef((props, ref) => (
    <Input placeholder="shadcn" />
  ));**

return (
<Form {... form}>
........
<FormControl>
   **<InputWithRefF {...field} />**
...........
)
};
export default MyFormComponent;

p.s i'm just starting with React, so I don't know if this code is 100% correct. it does solve the problem though. My setup: Vite React v18 + TS

JerrelS avatar May 03 '25 08:05 JerrelS

Solved by upgrading to React 19. In React 19, you can pass ref as a prop.

Thanks a lot 😊

There a some things even LLMs can't fix, this simple fix is one of them 😀

rahulharpal1603 avatar Jun 07 '25 19:06 rahulharpal1603

I had the same problem when using <Input /> Inside <Form />

This fixed it

function Input(
  { className, type, ...props }: React.ComponentProps<'input'>,
  ref: React.ForwardedRef<HTMLInputElement>,
) {
  return (
    <input
      ref={ref}
      // rest of shadcn props
      {...props}
    />
  );
}

// Forward ref using React.forwardRef after function definition
const ForwardedInput = React.forwardRef(Input);
ForwardedInput.displayName = 'Input';

export { ForwardedInput as Input };

ebreness avatar Jul 11 '25 03:07 ebreness

Hey @shadcn, I wanted to add more context to this issue as I'm experiencing the same problem. I've created a minimal reproduction on StackBlitz to help demonstrate it.

Environment:

  • React: 18.3.1
  • Framework: Vite
  • shadcn/ui components: button, popover, label, input added via the latest CLI.

Problem: When following the exact code example from the Popover documentation, the component fails to work and throws a console warning about refs. This happens because the default Button component generated by the CLI does not use React.forwardRef, which is required by Radix's Slot primitive when using the asChild prop in React 18.

Console Warning:

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Check the render method of `Primitive.button.SlotClone`.

Reproduction Link: Here is a StackBlitz that clearly reproduces the issue with a fresh install: shadcn-popover-react18-ref-issue

Confirmation of the Fix: I can confirm that manually editing src/components/ui/button.tsx to wrap the Button component in React.forwardRef completely resolves the issue.

It seems the CLI template for the Button component may need to be updated to include React.forwardRef by default to ensure out-of-the-box compatibility for React 18 users.

Hope this helps get the issue resolved

Knufle avatar Jul 21 '25 21:07 Knufle

Skeleton has this bug too, uses latest command line

npx shadcn@latest add skeleton -o

Fixed as

diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx
index d789105..1b82ace 100644
--- a/src/components/ui/skeleton.tsx
+++ b/src/components/ui/skeleton.tsx
@@ -1,15 +1,19 @@
+import * as React from "react"
+
 import { cn } from "@/lib/utils"
 
-function Skeleton({
-  className,
-  ...props
-}: React.HTMLAttributes<HTMLDivElement>) {
+const Skeleton = React.forwardRef<
+  HTMLDivElement,
+  React.HTMLAttributes<HTMLDivElement>
+>(({ className, ...props }, ref) => {
   return (
     <div
-      className={cn("animate-pulse rounded-md bg-gray-300 dark:bg-gray-600", className)}
+      ref={ref}
+      className={cn("animate-pulse rounded-md bg-slate-900/10 dark:bg-slate-50/10", className)}
       {...props}
     />
   )
-}
+})
+Skeleton.displayName = 'Skeleton'
 
 export { Skeleton }

zhoub avatar Aug 18 '25 08:08 zhoub

Subscribed. This is a pretty annoying bug in a lot of shadcn/ui components, like the Button component. As said before, upgrading to react 19 fixed it for me.

GainorB avatar Sep 28 '25 22:09 GainorB

Subscribed. This is a pretty annoying bug in a lot of shadcn/ui components, like the Button component. As said before, upgrading to react 19 fixed it for me.

This have worked for me too (becuase of https://react.dev/blog/2024/12/05/react-19#ref-as-a-prop)

elisherer avatar Nov 16 '25 14:11 elisherer