DEV Community

Discussion on: React Best Practices

Collapse
 
fnky profile image
Christian Petersen • Edited

There's a small caveat with using JSX.IntrinsicElements, which is that it always includes a ref prop, even though it won't work in reality. This type is also referring to a legacy type for class-based components (LegacyRef<T>).

The better choice would be to use React.[Element]HTMLAttributes<HTML[Element]Element>, where [Element] is replace with the element type that your component wraps over, or the more explicit ComponentPropsWithRef<"button"> / ComponentPropsWithoutRef<"button">.

I prefer the former, since you don't have to think about updating the type of your Props in case you either wrap or unwrap your component forwardRef, which will return the correct type.

To explain further: Using JSX.IntrinsicElements will give you incorrect type information for props:

type Props = JSX.IntrinsicElements["button"]
const Button = (props: Props) => <button {...props} />;
const usage = <Button ref={...} /> // ref?: LegacyRef<HTMLButtonElement>
Enter fullscreen mode Exit fullscreen mode

Passing anything to ref will throw a warning in development mode:

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

This is because it has not been forwarded by our wrapper component.

It also refers to the incorrect type LegacyRef<T> which accepts string which are legacy as mentioned in the Ref and the DOM.

It's by built-in JSX elements to be backwards-compatible with earlier versions of the React API.

However, using the React.[Element]HTMLAttributes<HTML[Element]Element> instead:

type Props = React.ButtonHTMLAttributes<HTMLButtonElement>;
const Button = (props: Props) => <button {...props} />;
const usage = <Button ref={...} /> // no ref prop on Button, which is correct.
Enter fullscreen mode Exit fullscreen mode

Gives us the correct type information; that ref is not present since it doesn't take any refs.

If you'd like to accept a ref and pass it down to the underlying element, you want to use forwardRef:

type Props = React.ButtonHTMLAttributes<HTMLButtonElement>;
const Button = React.forwardRef<HTMLButtonElement, Props>((props, forwardedRef) => {
  return <button ref={forwardedRef} {...props} />;
});
const usage = <Button ref={...} /> // ref?: Ref<HTMLButtonElement>
Enter fullscreen mode Exit fullscreen mode

Note that the type for ref prop is now Ref<T>, which conforms correctly to what useRef and createRef returns (RefCallback<T> | RefObject<T>).

Hope this helps :-)