Компонент React высшего порядка для отслеживания кликов

Я хотел бы реализовать компонент реакции более высокого порядка, который можно использовать для простого отслеживания событий (например, щелчка) в любом компоненте React. Это делается для того, чтобы легко отслеживать клики (и другие события) в нашем собственном аналитическом трекере.

Проблема, с которой я столкнулся, заключается в том, что система синтетических событий React требует, чтобы события (например, onClick) были привязаны к реагированию на элементы DOM, такие как div. Если компонент, который я обертываю, является настраиваемым компонентом, как и любой HOC, реализованный с помощью функции более высокого порядка, мои события щелчка не связываются правильно.

Например, при использовании этого HOC обработчик onClick будет срабатывать для button1, но не для button2.

// Higher Order Component 
class Track extends React.Component {
  onClick = (e) => {
    myTracker.track(props.eventName);
  }

  render() {
    return React.Children.map(
      this.props.children,
      c => React.cloneElement(c, {
        onClick: this.onClick,
      }),
    );
  }
}

function Wrapper(props) {
  return props.children;
}

<Track eventName = {'button 1 click'}>
  <button>Button 1</button>
</Track>

<Track eventName = {'button 2 click'}>
  <Wrapper>
    <button>Button 2</button>
  </Wrapper>
</Track>

CodeSandbox с рабочим примером:https://codesandbox.io/embed/pp8r8oj717

Моя цель - иметь возможность использовать такой HOF (необязательно в качестве декоратора) для отслеживания кликов по любому определению компонента React.

export const withTracking = eventName => Component => props => {
  return (
    <Track eventName = {eventName}>
      {/* Component cannot have an onClick event attached to it */}
      <Component {...props} />
    </Track>
  );
};

Единственное решение, которое я могу придумать для банкомата, - это использовать Ref для каждого дочернего элемента и вручную связывать мое событие щелчка после заполнения Ref.

Любые идеи или другие решения приветствуются!

Обновлено: Используя технику remapChildren из ответа @estus и более ручной способ рендеринга обернутых компонентов, я смог заставить это работать как функцию более высокого порядка - https://codesandbox.io/s/3rl9rn1om1

export const withTracking = eventName => Component => {
  if (typeof Component.prototype.render !== "function") {
    return props => <Track eventName = {eventName}>{Component(props)}</Track>;
  }
  return class extends Component {
    render = () => <Track eventName = {eventName}>{super.render()}</Track>;
  };
};
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
9
0
1 852
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Невозможно получить ссылку на базовый узел DOM (а их может быть несколько) из элемента Wrapper React обычными средствами, такими как ref или ReactDOM.findDOMNode. Для этого необходимо рекурсивно обходить дочерние элементы.

пример:

remapChildren(children) {
  const { onClick } = this;

  return React.Children.map(
    children,
    child => {   
      if (typeof child.type === 'string') {
        return React.cloneElement(child, { onClick });
      } else if (React.Children.count(child.props.children)) {
        return React.cloneElement(child, { children: this.remapChildren(
          child.props.children
        ) });
      }
    }
  );
}

render() {
  return this.remapChildren(this.props.children);
}

remapChildren размещает onClick только на элементах DOM. Обратите внимание, что эта реализация пропускает вложенные элементы DOM.

Это довольно умно. Если бы я мог подвести итог, мы рекурсивно просматривали каждого дочернего элемента для первого визуализированного элемента DOM (определяемого по его типу, являющемуся строкой), присоединяя обработчик onClick. Сам дочерний элемент может иметь много дочерних элементов, возможно, со смешанными типами, но это учитывается с помощью рекурсивного метода, который выполняет итерацию по всем дочерним элементам и проверяет их типы.

jamis0n 08.10.2018 21:54

Да, это правильно. Полагаю, это единственный способ, кроме прямого доступа к DOM.

Estus Flask 08.10.2018 21:59

Просто протестировал этот подход, и он отлично работает с приведенными выше вариантами использования <Wrapper>, но не как HOC в случае withTracking, поскольку Component при рендеринге в HOC не имеет дочерних элементов (<Component {...props} />).

jamis0n 09.10.2018 03:55

Похоже, рендеринг детей через React.createElement не работает с функцией remapChildren. Однако использование super.render() с компонентом класса и вызов функционального компонента вместо этого работает (см. Обновленный пример).

jamis0n 09.10.2018 06:30

Да, это потому, что отрендеренные дочерние элементы компонента не отображаются как props.children. Полагаю, это приемлемый хакер. Обратите внимание, что компонент не обязательно является функцией. Таким образом, вы ограничены функциями, но есть множество других компонентов, которые часто встречаются в реальном приложении, например контекст (также контекст может встречаться в любом месте иерархии элементов). Это хорошее подтверждение концепции, но я не уверен, насколько это практично. Было бы, вероятно, более эффективно получить детей с прямым доступом к DOM и напрямую установить / отключить прослушиватель click. Зависит от использования Track.

Estus Flask 09.10.2018 08:53
Component в варианте использования withTracking - это компонент определение (withTracking(eventName)(Component)). Это объясняет, что это определение является либо функцией, либо классом. Когда компонент React, который вы хотите украсить этой функциональностью, не будет определяться ни как функция, ни как класс?
jamis0n 09.10.2018 16:19

Я имею в виду, с какими именно компонентами он будет использоваться. Вы будете ограничены компонентами, которые были специально разработаны для бесперебойной работы с Track. Любой сторонний компонент может быть незамеченным. Примерами компонентов, которые не являются функциями, являются React.forwardRef или контекстный Consumer. Даже если внешний компонент является функцией, он может содержать компонент, который не раскрывает props.children. Кстати, render = () => нехорошая штука, лучше сделать прототип методом по соображениям совместимости, render() {.

Estus Flask 09.10.2018 16:33

Понятно. Это хорошее пояснение, что это не ограничение HOF, а скорее Track в целом. Для моих случаев использования это должно охватывать почти все, что нам нужно.

jamis0n 09.10.2018 16:44

Другие вопросы по теме