DevBlog of Árpád Poprádi

JSX for Functionality

React recommends JSX to describe the UI but JSX is more than a nice syntax to convey DOM. JSX defines React elements instead of DOM elements and with that it combines the full power of React components such as lifecycle methods, state and children manipulation with a lightweight tag-based syntax. This allows us to factor out features without any visible HTML elements into React components and use them by composition.

For example it can be useful to get notified about a click outside of a component. That sounds like a general functionality independent from the specific component so it would be nice to have it as something reusable in a simple syntax. Like this JSX:

<OutsideClickDetector onOutsideClick={handleOutsideClick}>
  <Component/>
</OutsideClickDetector>

How to build OutsideClickDetector completely independent from Component? Which building blocks are needed?

I go through the needed features and how they can be implemented in OutsideClickDetector.

  1. Detect the click
  2. Determine whether the click is outside of Component
  3. Notify about the outside click

Detect the click

class OutsideClickDetector extends React.Component {
  onClick = event => {
  //implemented later 
  }

  componentDidMount() {
    document.addEventListener('click', this.onClick);
  }

  componentWillUnmount() {
    document.removeEventListener('click', this.onClick);
  }
}

We detect click on the whole document and later check whether the click is outside of the Component. onClick is defined with the ES2015 class property syntax. These React lifecycle methods allow us to be notified only if the component is mounted.

Determine whether the click is outside of Component

To accomplish that we need the Component's DOM element.

render() {
  const child = React.Children.only(this.props.children);
  
  return React.cloneElement(child, {
    ref: node => {
      this.childRef = node;
    },
  });
}

The first statement is for safety. For the sake of simplicity only one child is allowed and we want to ensure it. React.Children.only(this.props.children) throws an error if this.props.children has more children otherwise returns the one child.

The beauty is in the next statement because that is why Component doesn't need to know anything about OutsideClickDetector. OutsideClickDetector manipulates its child to its own need by rendering an extended child element.Component's ref will be stored in the state of OutsideClickDetector. And all this behind the child's back.

Normally the JSX tree syntax becomes tree of React components in the render lifecycle method. The fate of the children is completely in the hand of the parent. The parent can ignore, replace or manipulate the children.

There is another interesting aspect in this render method. Our OutsideClickDetector doesn't render UI elements on its own, it only attaches some functionality to its child.

Practicaly we attach a non-visible, statefull, lifecycle method triggered object to the child.

Ok, next the decision in the onClick method:

onClick = event => {
  if (!this.childRef) {
    return;
  }

  if (this.childRef === event.target) {
    return;
  }

  if (this.childRef.contains(event.target)) {
    return;
  }
  
  //notify about an outside click
}

Here we compare Component's reference with the event.target of the click. A click is an outside click if it neither on the Component nor inside the Component.

Notify about an outside click

class OutsideClickDetector extends Component {
  static propTypes = {
    onOutsideClick: PropTypes.func.isRequired,
  }
  
  onClick = event => {
  //like above
  this.props.onOutsideClick();
  }
  
  //other methods like above
}
  

Summary

JSX discreetly packs the power of React components into a tag-based syntax. In the render lifecycle method we can hook up code to the UI assembly. It opens up the door for child manipulation to attach a non-visible, statefull, lifecycle method triggered object to the child implementing non-visible feature extensions in harmony with the child's lifecycle.