Использование ref с React.createElement

У меня есть многоразовый компонент заголовка, который позволяет мне передавать tag prop, создавая любой заголовок (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>;
};

Тем не менее, я сталкиваюсь с вариантом использования, когда я хотел бы иметь ref, используя хук useRef(), на Tag, чтобы я мог получить доступ к элементу и анимировать с помощью 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 к компоненту?

Спасибо.


person Jesse Winton    schedule 09.03.2021    source источник


Ответы (2)


Используйте разброс объектов, чтобы добавить 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>

person Ori Drori    schedule 09.03.2021

Вам нужно перенаправить 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, ref) => {
    return (
      <Tag className={s(s.heading, className)} ref={headingRef}>
        {children}
      </Tag>
    );
  }
);

export const HeadingWithoutJSX = forwardRef(
  ({ tag, children, className }: HeadingProps, ref) => {
    return createElement(
      tag,
      { className: s(s.heading, className), ref },
      children
    );
  }
);
person Mr. Hedgehog    schedule 09.03.2021
comment
Я возился с этим, но передать ли его компоненту Heading или Tag? - person Jesse Winton; 09.03.2021
comment
Я добавил примеры кода с jsx и без него. Но я убрал промежуточный компонент, так как он не нужен. - person Mr. Hedgehog; 09.03.2021