Updating Shadcn UI Components to React 19

In React 19, the 'forwardRef' function was deprecated. This post will show you how to update your Shadcn UI components to work with React 19 and ensure future compatibility.

In React 19, the forwardRef function was deprecated.

This post will show you how to update your Shadcn UI components, so that they work with React 19, but also to simplify the code to pass references to components.

How did Shadcn UI components work before?

Shadcn UI is one of the most popular UI libraries for React. Based on the incredible work of Radix UI, Shadcn UI introduced a set of "open" components styled with Tailwind CSS that took the React ecosystem by storm.

Shadcn UI uses the forwardRef function to pass references to components. This is a common pattern in React, and it's how we can pass a reference to a component to a child component.

As of React 19, the forwardRef function is deprecated. Instead, ref is now a prop that can be passed to a component.

function SuperButton({id, ref}) {
return <button id={id} ref={ref} />
}
//...
<SuperButton ref={ref} />

The new approach simplifies component composition by removing the need for forwardRef. Components can now directly accept and pass refs as props, making the code more straightforward and maintainable.

How to update Shadcn UI components to work with React 19

To update your Shadcn UI components to work with React 19, you need to update the forwardRef function to ref and pass the ref as a prop to the component.

Here's an example of how to update the Input component. Before, it used forwardRef to pass a reference to the component to the Label component:

import * as React from 'react';
import { cn } from '../lib/utils';
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type = 'text', ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
ref={ref}
{...props}
/>
);
},
);
Input.displayName = 'Input';
export { Input };

After updating the Input component to use ref instead of forwardRef, you can pass the ref as a prop to the Label component:

import * as React from 'react';
import { cn } from '../lib/utils';
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
const Input: React.FC<InputProps> = ({
className,
type = 'text',
...props
}) => {
return (
<input
type={type}
className={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
/>
);
};
Input.displayName = 'Input';
export { Input };

In the above code, we've updated the Input component to use ref instead of forwardRef. We've also updated the Input component to accept a type prop and pass it to the input element.

Because ref is already a prop, we don't need to specify it because we spread the props object, and ref is already a property of props.

Updating a more complex component in Shadcn UI

Let's take a look at an example of how to update a more complex component in Shadcn UI.

The Popover component is an example of a component that uses forwardRef to pass a reference to the component to a child component and extends the PopoverPrimitive.Root component from Radix UI.

'use client';
import * as React from 'react';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { cn } from '../lib/utils';
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
const PopoverContent: React.FC<
React.ComponentProps<typeof PopoverPrimitive.Content>
> = ({ className, align = 'center', sideOffset = 4, ...props }) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
));
);
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

Insead of using forwardRef, we can use ref directly in the PopoverContent component:

'use client';
import * as React from 'react';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { cn } from '../lib/utils';
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverAnchor = PopoverPrimitive.Anchor;
const PopoverContent: React.FC<
React.ComponentProps<typeof PopoverPrimitive.Content>
> = ({ className, align = 'center', sideOffset = 4, ...props }) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
);
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };

As you can see from the above snippet, we have updated the component to use ref instead of forwardRef. We have also updated the PopoverContent component to accept a ref as a prop and pass it to the PopoverPrimitive.Content component.

To extend a Radix UI Primitive, use the pattern below:

type Props = React.ComponentProps<typeof RadixPrimitive>;
const NewComponent = React.FC<Props> = // Your component code...

Where NewComponent is the name of your component and RadixPrimitive is the name of the Radix UI Primitive you want to extend.

Conclusion

In this post, we've seen how to update Shadcn UI components to work with React 19, while also writing less code to achieve the same result.

Makerkit, the best Next.js SaaS Starter Kit for building SaaS products, uses Shadcn UI components extensively. We've already updated our components to work with React 19 and ensure compatibility with future versions of React.