Я хотел бы реализовать компонент реакции более высокого порядка, который можно использовать для простого отслеживания событий (например, щелчка) в любом компоненте 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>;
};
};



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Невозможно получить ссылку на базовый узел 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.
Просто протестировал этот подход, и он отлично работает с приведенными выше вариантами использования <Wrapper>, но не как HOC в случае withTracking, поскольку Component при рендеринге в HOC не имеет дочерних элементов (<Component {...props} />).
Похоже, рендеринг детей через React.createElement не работает с функцией remapChildren. Однако использование super.render() с компонентом класса и вызов функционального компонента вместо этого работает (см. Обновленный пример).
Да, это потому, что отрендеренные дочерние элементы компонента не отображаются как props.children. Полагаю, это приемлемый хакер. Обратите внимание, что компонент не обязательно является функцией. Таким образом, вы ограничены функциями, но есть множество других компонентов, которые часто встречаются в реальном приложении, например контекст (также контекст может встречаться в любом месте иерархии элементов). Это хорошее подтверждение концепции, но я не уверен, насколько это практично. Было бы, вероятно, более эффективно получить детей с прямым доступом к DOM и напрямую установить / отключить прослушиватель click. Зависит от использования Track.
Component в варианте использования withTracking - это компонент определение (withTracking(eventName)(Component)). Это объясняет, что это определение является либо функцией, либо классом. Когда компонент React, который вы хотите украсить этой функциональностью, не будет определяться ни как функция, ни как класс?
Я имею в виду, с какими именно компонентами он будет использоваться. Вы будете ограничены компонентами, которые были специально разработаны для бесперебойной работы с Track. Любой сторонний компонент может быть незамеченным. Примерами компонентов, которые не являются функциями, являются React.forwardRef или контекстный Consumer. Даже если внешний компонент является функцией, он может содержать компонент, который не раскрывает props.children. Кстати, render = () => нехорошая штука, лучше сделать прототип методом по соображениям совместимости, render() {.
Понятно. Это хорошее пояснение, что это не ограничение HOF, а скорее Track в целом. Для моих случаев использования это должно охватывать почти все, что нам нужно.
Это довольно умно. Если бы я мог подвести итог, мы рекурсивно просматривали каждого дочернего элемента для первого визуализированного элемента DOM (определяемого по его типу, являющемуся строкой), присоединяя обработчик
onClick. Сам дочерний элемент может иметь много дочерних элементов, возможно, со смешанными типами, но это учитывается с помощью рекурсивного метода, который выполняет итерацию по всем дочерним элементам и проверяет их типы.