Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Higher Order React Component for Click Tracking

I'd like to implement a higher order react component which can be used to easily track events (like a click) on any React component. The purpose of this is to easily hook clicks (and other events) into our first party analytics tracker.

The challenge I've come across is that the React synthetic event system requires events (like onClick) be bound to react DOM elements, like a div. If the component I'm wrapping is a custom component, like every HOC implemented via a higher order function is, my click events don't bind correctly.

For example, using this HOC, the onClick handler will fire for button1, but not for 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 with working example: https://codesandbox.io/embed/pp8r8oj717

My goal is to be able to use an HOF like this (optionally as a decorator) to track clicks on any React component definition.

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

The only solution I can think of atm is using a Ref on each child and manually binding my click event once the Ref is populated.

Any ideas or other solutions are appreciated!

UPDATE: Using the remapChildren technique from @estus' answer and a more manual way of rendering the wrapped components, I was able to get this to work as a higher order function - 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>;
  };
};
like image 621
jamis0n Avatar asked Oct 08 '18 15:10

jamis0n


People also ask

How do you track clicks in React?

To install react-tracking, we first need to install React. We'll create a small program that will track the button click event in a functional and class component. The class button will also track the number of clicks.

What is the higher-order component generally used for in React?

A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React's compositional nature. Concretely, a higher-order component is a function that takes a component and returns a new component.

What are higher-order components in React example?

Higher-Order Components are not part of the React API. They are the pattern that emerges from React's compositional nature. The component transforms props into UI, and a higher-order component converts a component into another component. The examples of HOCs are Redux's connect and Relay's createContainer.


1 Answers

It's impossible to get a reference to underlying DOM node (and there can be more than one of them) from Wrapper React element by regular means, like a ref or ReactDOM.findDOMNode. In order to do that, element children should be traversed recursively.

An example:

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 places onClick only on DOM elements. Notice that this implementation skips nested DOM elements.

like image 94
Estus Flask Avatar answered Oct 14 '22 14:10

Estus Flask