Jesse Winton
Jesse Winton

Reputation: 696

Using ref with React.createElement

I've got a reusable heading component that allows me to pass a tag prop, creating any sort of heading (h1, h2, h3 etc). Here's that component:

heading.tsx

import React, { ReactNode } from 'react';

import s from './Heading.scss';

interface HeadingProps {
  tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
  children: ReactNode;
  className?: string;
}

export const Heading = ({ tag, children, className }: HeadingProps) => {
  const Tag = ({ ...props }: React.HTMLAttributes<HTMLHeadingElement>) =>
    React.createElement(tag, props, children);

  return <Tag className={s(s.heading, className)}>{children}</Tag>;
};

However, I'm coming across a use case where I'd like to be able to have a ref, using the useRef() hook, on the Tag, so that I can access the element and animate with GSAP. However, I can't figure out how do this using createElement.

I've tried to do it by adding a ref directly to the Tag component, and adding it to the Tag props like so:

import React, { ReactNode, useRef } from 'react';

import s from './Heading.scss';

interface HeadingProps {
  tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
  children: ReactNode;
  className?: string;
}

export const Heading = ({ tag, children, className }: HeadingProps) => {
  const headingRef = useRef(null);
  const Tag = ({ ...props }: React.HTMLAttributes<HTMLHeadingElement>) =>
    React.createElement(tag, props, children, {ref: headingRef});

  return <Tag className={s(s.heading, className)} ref={headingRef}>{children}</Tag>;
};

I receive the error Property 'ref' does not exist on type 'IntrinsicAttributes & HTMLAttributes<HTMLHeadingElement>'.

What am I doing wrong, and how can I safely add a ref to the component?

Thanks.

Upvotes: 2

Views: 6242

Answers (2)

Mr. Hedgehog
Mr. Hedgehog

Reputation: 2885

You need to forward ref and then pass it not as a child or element, but as it's property.

Here is documentation on ref forwarding: https://reactjs.org/docs/forwarding-refs.html

Here is code exapmles for heading without intermediate component creation:

import React, { ReactNode, useRef } from "react";

import s from "./Heading.scss";

interface HeadingProps {
  tag: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
  children: ReactNode;
  className?: string;
}

export const Heading = forwardRef(
  ({ tag: Tag, children, className }: HeadingProps, headingRef) => {
    return (
      <Tag className={s(s.heading, className)} ref={headingRef}>
        {children}
      </Tag>
    );
  }
);

export const HeadingWithoutJSX = forwardRef(
  ({ tag, children, className }: HeadingProps, headingRef) => {
    return createElement(
      tag,
      { className: s(s.heading, className), ref: headingRef},
      children
    );
  }
);

Upvotes: 2

Ori Drori
Ori Drori

Reputation: 192006

Use object spread to add the ref to the props:

const { useRef, useEffect } = React;

const Heading = ({ tag, children, className }) => {
  const headingRef = useRef(null);
  const Tag = (props) => React.createElement(tag, {ref: headingRef, ...props }, children);
  
  useEffect(() => { console.log(headingRef); }, []); // demo - use the ref

  return <Tag>{children}</Tag>;
};

ReactDOM.render(
  <div>
    <Heading tag="h1">h1</Heading>
    <Heading tag="h2">h2</Heading>
    <Heading tag="h3">h3</Heading>
  </div>,
  root
);
.as-console-wrapper { max-height: 100% !important; top: 0; left: 50% !important; }
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

<div id="root"></div>

However, creating a component inside another component would mean that component would be recreated on each render. You can avoid that by using useMemo(). However an easier option would be to render the tag itself as JSX:

const { useRef, useEffect } = React;

const Heading = ({ tag: Tag, children, className }) => {
  const headingRef = useRef(null);
  
  useEffect(() => { console.log(headingRef); }, []); // demo - use the ref

  return <Tag className={className} ref={headingRef}>{children}</Tag>;
};

ReactDOM.render(
  <div>
    <Heading tag="h1">h1</Heading>
    <Heading tag="h2">h2</Heading>
    <Heading tag="h3">h3</Heading>
  </div>,
  root
);
.as-console-wrapper { max-height: 100% !important; top: 0; left: 50% !important; }
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

<div id="root"></div>

Upvotes: 4

Related Questions