Climbing Up React Component Tree

in react

React virtual DOM is great at calculating differences and rendering DOM trees efficiently. React also provides us with props.children which helps to traverse down a component tree. But when you need to climb up, there's no matching parent property. In the following post I'll try to work around that limitation and calculate an array based on a component's parents.

The Task: Calculate path down to a React component

Assume the following component tree:

<div>
  <input />
  <Thing mark="5">
    <div>
      <OtherThing mark="8">
        <Thing/>
      </OtherThing>
      <Thing>
        <Thing mark="9" />
      </Thing>
    </div>
  </Thing>
</div>

We'd like to allow the components Thing and OtherThing to access all mark values from the root down to the components themselves. So in our example we'd like to have the first Thing identified by [5], the first OtherThing identified by [5,8] and the last Thing identified by [5,9].

In vanilla JavaScript we'd use a function that looks something like this:

function climbUp(el) {
  const path = [];

  while(true) {
    let mark = el.getAttribute('mark');
    if (mark) {
      path.push(mark);
    }
    if (!el.parentElement) {
      return path;
    }

    el = el.parentElement;
  }
}

Unfortunately, React virtual DOM doesn't provide an equivalent to parentElement. Let's try to work around that by sending the data down the tree.

Take 1: Using props

One approach could be to pass mark as prop from parent to children, modifying the value to use an array as we pass it down. By using a higher order component we can automate this process, as illustrated in the following snippet:

function withMark(Component) {
  return class extends React.Component {
    render() {
      const previousMarks = this.props.previousMarks || [];
      const nextMark = this.props.mark ? [...previousMarks, this.props.mark] : previousMarks;
      
      return (
        <Component {...this.props} mark={nextMark}>
          {React.Children.map(this.props.children, child => (
            React.cloneElement(child, { previousMarks: nextMark })
          ))}
        </Component>
      );
    }
  }
}

We can see the code in action in the codepen below (click "Edit On Codepen" to see the full source code for the demo):

See the Pen KvgPGv by Ynon Perek (@ynonp) on CodePen.

But it only takes a moment to realize the main drawback in the solution: ALL intermediate nodes must be decorated with the higher order component. Inserting a div (or any other component) between the outer and inner Thing breaks the path.

Take 2: Using Context

By using context we can get around the limitations of the previous approach and build a working solution, as seen in the next snippet:

function withMark(Component) {
  const res = class Marker extends React.Component {
    getChildContext() {
      return {
        marks: this.getMarks(),
      }
    }

    getMarks() {
      const previousMarks = this.context.marks || [];
      return this.props.mark ? [...previousMarks, this.props.mark] : previousMarks;
    }

    render() {
      return (
        <Component {...this.props} mark={this.getMarks()} />
      );
    }
  }
  res.childContextTypes = {
    marks: PropTypes.array,
  };
  res.contextTypes = {
    marks: PropTypes.array,
  };
  return res;
}

This works much better as we can now use non-decorated intermediate components. Here's the full working code:

See the Pen PKNrKE by Ynon Perek (@ynonp) on CodePen.

Using context to pass our data means the solution still has one major limitation: It must use constant values for mark, as changing data in parent's context does not necessarily re-render its children. We can see a live demo of the problem in the following codepen:

See the Pen RZKbqd by Ynon Perek (@ynonp) on CodePen.

Hit "tick" several times and you'll see how only one of the components change.

Take 3: Using context AND events

To work around that final limitation and be able to use mutating data in context, we'll need to change the spec. While we can't guarantee render() will be called whenever context changes, it is possible to read the correct values when we need them in response to another event.

This means if we only need the path in response to a user event and at the time of that event, we can save references to each parent component in context, and read the data only when an event is fired. Here's a working example that does just that:

See the Pen prRoZa by Ynon Perek (@ynonp) on CodePen.

Every time you click "tick" only the first component's render() function is called, due to the context limitation we discussed earlier. But now we can use our new "Re-calc" button to refresh the values. This is good enough if you only need the values in response to user events.

This solution also provides a generic way for React components to access their parents, as long as all components are decorated with the same HOC. This last solution yields exactly the same functionality we had in the vanilla JS snippet we started with: i.e. climbing up a component tree on demand.

Final Thoughts

React architecture is based on the idea of unidirectional data flow, i.e. having the data defined in each parent and flow down to its children. That's why traversing up from a child to its parent is considered bad practice.

That said, I really like how React provides us with the tools to solve even non-standard problems while staying inside the lines and not using any undocumented APIs. This is part of what makes programming in React actually fun.

Comments