skip to Main Content

I’ve been using React 19, and with the changes to how ref is handled, I noticed I need to adjust my code for components that inherit standard HTML element attributes.

Previously, I used forwardRef like this, and it worked perfectly, passing all attributes of a <div> to the component.

Using forwardRef

export const Section = forwardRef<
  HTMLDivElement,
  AllHTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
  <div {...props} className={clsx("", className)} ref={ref} />
));

Section.displayName = "Section";

Using Ref

export const Section = ({
  className,
  ref,
  ...props
}: {
  className?: string;
  children: ReactNode;
  ref?: Ref<HTMLDivElement>;
}) => <div className={clsx("section", className)} ref={ref} {...props} />;

Now, to achieve the same behavior, I have to write something more verbose, creating an interface for each component:

interface SectionProps extends AllHTMLAttributes<HTMLDivElement> {
  className?: string;
  children?: ReactNode;
  ref?: Ref<HTMLDivElement>;
}

This works, but it makes the code more repetitive. My question is: if I have multiple components like Card, CardHeader, CardTitle, CardContent, etc., do I need to create an interface for each one of them?

Any ideas on how to fix this?

I’m looking for a way to simplify this process while keeping the TypeScript type safety without having to define a separate interface for every single component.

What I’ve tried:

  • Checked the latest React 19 documentation.
  • Considered creating a base interface to reuse, but I’m not sure if that’s the best practice.

2

Answers


  1. Chosen as BEST ANSWER

    While exploring how to handle HTML attributes and ref properly in React 19, I found an efficient solution to my question.

    The Problem

    The challenge was enabling a component to receive standard HTML attributes like className and id while properly handling the ref following the changes introduced in React 19.

    The Solution

    I created a base interface that simplifies reuse and ensures that all attributes are correctly assigned. Here's how it works:

    Defining a Base Interface

    import {AllHTMLAttributes, ReactNode, Ref} from "react"
    
    interface BaseProps<T extends HTMLElement> extends AllHTMLAttributes<T> {
    className: string;
    children: ReactNode;
    ref: Ref<T>
    };
    

    This interface:

    • Extends AllHTMLAttributes<T>: Inherits all standard HTML attributes for the specified type.
    • Defines Custom Props: Includes properties like className, children, and ref, as needed.

    Applying the Interface in a Component

    With the interface defined, you just need to specify the type of HTML element in the component for correct behavior:

    import clsx from "clsx"
    
    export const SectionTitle = ({className, ref, ...props}: BaseProps<HTMLHeadingElement>) => (
        <h2 className={clsx("section-title", className)} ref={ref} {...props}/>
    );
    

    Why It Works Great

    1. Reusable: The interface can be applied to many components, reducing repetitive code.
    2. Type-Safe: All standard HTML attributes are available and correctly typed.
    3. Flexible: This approach is easy to adapt for other, more complex components.

    Final Thoughts

    This solution has worked well for me, and I hope it helps other developers with similar challenges.


  2. interface SectionProps extends AllHTMLAttributes<HTMLDivElement> {
      className?: string;
      children?: ReactNode;
      ref?: Ref<HTMLDivElement>;
    }
    

    This works, but it makes the code more repetitive. My question is: if
    I have multiple components like Card, CardHeader, CardTitle,
    CardContent, etc., do I need to create an interface for each one of
    them?

    I’m looking for a way to simplify this process while keeping the
    TypeScript type safety without having to define a separate interface
    for every single component.

    Generally speaking you’d very likely want separate interfaces for all these components, but you can abstract the common props they share into an interface they all can extend before adding their specific properties.

    children, classname, and ref are all common React props, you could create an interface that includes these by default using React’s PropsWithChildren type:

    import type {
      AllHTMLAttributes,
      Ref,
      PropsWithChildren,
    } from "react";
    
    interface AllHTMLAttributesDivProps
      extends PropsWithChildren<AllHTMLAttributes<HTMLDivElement>> {
      ref?: Ref<HTMLDivElement>;
    }
    
    interface SectionProps extends AllHTMLAttributesDivProps {
      // add section component specific props here
    }
    
    const Section = ({ className, ...props }: SectionProps) => (
      <div className={clsx("section", className)} {...props} />
    );
    
    Section.displayName = "Section";
    

    If you didn’t want to "lock" yourself into only using all HTML attributes on a div element you can make your common interface generic instead:

    interface AllHTMLAttributesProps<T = HTMLDivElement>
      extends PropsWithChildren<AllHTMLAttributes<T>> {
      ref?: Ref<T>;
    }
    

    Using a div element implementation (the default/fallback type):

    interface SectionProps extends AllHTMLAttributesProps {
      // add section component specific props here
    }
    
    const Section = ({ className, ...props }: SectionProps) => (
      <div className={clsx("section", className)} {...props} />
    );
    
    Section.displayName = "Section";
    

    Using a span element implementation:

    interface SectionProps extends AllHTMLAttributesProps<HTMLSpanElement> {
      // add section component specific props here
    }
    
    const Section = ({ className, ...props }: SectionProps) => (
      <span className={clsx("section", className)} {...props} />
    );
    
    Section.displayName = "Section";
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search