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.
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>