Reputation: 696
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
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
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