将 ref 与 React.createElement 一起使用

Using ref with React.createElement

我有一个可重复使用的标题组件,它允许我传递 tag 道具,创建任何类型的标题(h1、h2、h3 等)。这是该组件:

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>;
};

但是,我遇到了一个用例,我希望能够在 Tag 上使用 useRef() 挂钩 ref,所以我可以访问该元素并使用 GSAP 制作动画。但是,我不知道如何使用 createElement.

我尝试通过将 ref 直接添加到 Tag 组件,并将其添加到 Tag 属性来实现,如下所示:

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>;
};

我收到错误 Property 'ref' does not exist on type 'IntrinsicAttributes & HTMLAttributes<HTMLHeadingElement>'.

我哪里做错了,我怎样才能安全地将 ref 添加到组件?

谢谢。

您需要转发 ref 然后将其作为 child 或元素传递,而是作为 属性.

传递

这是关于 ref 转发的文档:https://reactjs.org/docs/forwarding-refs.html

这里是没有创建中间组件的标题的代码示例:

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
    );
  }
);

使用对象传播将 ref 添加到 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>

但是,在另一个组件内创建一个组件意味着将在每次渲染时重新创建该组件。您可以使用 useMemo() 来避免这种情况。然而,一个更简单的选择是将 tag 本身呈现为 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>