Back to index

Sweet Render Hijacking with React

June 4th, 2018

6 min read

Banner image

I’d like to share with you some obscure pattern that I’ve recently used in one of my projects. Brace yourself, because what you are about to see may hurt your eyes (or delight otherwise). For starters, let’s refresh our knowledge about Higher Order Components (HOC).

In a nutshell, a HOC is just a React Component that wraps another one. Typical use cases of this would be manipulating props, abstracting state, accessing the instance via Refs or wrapping the WrappedComponent with other elements. The signature of a standard HOC is as follows:

function enhancer(WrappedComponent) {
  return class EnhancedComponent extends React.Component {
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}

Let’s take a look at a possible implementation for abstracting state, which could be sharing some authentication state needed by several components:

function withAuth(WrappedComponent) {
  return class EnhancedComponent extends React.Component {
    state = {
      isLoggedIn: verifyAuth(),
    };

    // More code in between

    render() {
      const newProps = {
        isLoggedIn: this.state.isLoggedIn,
      };
      return <WrappedComponent {...this.props} {...newProps} />;
    }
  };
}

The key thing here is that the render method of the HOC returns a React Element of the type of WrappedComponent.

The use case

I recently joined a React Native project that was already under active development and got tasked with implementing some tabbed collapsible header. The Tab pages were ScrollView components at their core and were not only used under the tabs, but also in other places.

We had almost 20 of them and some needed in certain cases to be animatable, which meant to replace the top most parent ScrollView with its animated version Animated.ScrollView and add some extra props for hooking up scroll events to animated values.

As a DRY advocate, I wanted to avoid touching all those files individually in order to set up some extra conditional logic, to either return ScrollView or Animated.ScrollView as the top most component from the render function.

Instead, I was seeking for a single utility that allowed me to swap them on demand.

Can we achieve this goal with a standard HOC?

If we are familiar with the difference between components, their instances, and elements in React, you could think of creating some HOC that creates a WrappedComponent element (by calling React.createElement or using JSX) and then somehow grab its children. Something along these lines:

function withAnimatedScrollView(WrappedComponent) {
  return class Enhancer extends React.Component {
    render() {
      const elementsTree = <WrappedComponent {...this.props} />;
      const { children, scrollY, ...rest } = elementsTree.props;
      return (
        <Animated.ScrollView
          {...rest}
          onScroll={Animated.event(
            [{ nativeEvent: { contentOffset: { y: scrollY } } }],
            {
              useNativeDriver: true,
            }
          )}
          scrollEventThrottle={1}
        >
          {children}
        </Animated.ScrollView>
      );
    }
  };
}

However, this won’t work because <WrappedComponent /> is a plain object describing a component instance and its desired properties. It contains only information about the component type (the displayName we used), its properties and any child elements inside it, but we can’t see beyond that.

In order to get a shallow representation of what’s WrappedComponent output, we need to call its render method.

Well...I did mention that one of the use cases of a standard HOC is accessing the instance via Refs right? Great, so you may be thinking that would allow us to control the WrappedComponent and do all sort of things with it, like calling its render.

Hold your horses' cowboy.

"You can access this (the instance of the WrappedComponent) with a Ref, but you will need a full initial render process of the WrappedComponent for the ref to be calculated, that means that you need to return the WrappedComponent element from the HOC render method, let React do its reconciliation process and just then you will have a Ref to the WrappedComponent instance."

That means WrappedComponent will have to be already rendered on the screen, with the output that it presented initially.

Ok, we know the problem and we are getting closer to what we need. The question that needs to be asked is: is there a way to access the WrappedComponent instance before React reconciles its render output?

Let me kindly introduce you to the Inheritance Inversion HOC.

Inheritance Inversion HOC

Inheritance inversion HOC is implemented by returning a class component that extends the passed WrappedComponent, instead of React.Component. In this way the relationship between them seems inverse.

function enhancer(WrappedComponent) {
  return class EnhancedComponent extends WrappedComponent {
    render() {
      const elementsTree = super.render();
      // Do something with elementsTree
      return {...}
     }
  };
}

This pattern allows the HOC to have access to the WrappedComponent instance via this, which means it has access to the state, props, component lifecycle hooks and more importantly, the render method.

Voila! Now we are able to create a HOC that can get the render tree of WrappedComponent, swap the top most parent component and keep the children intact, by using this approach.

function withAnimatedScrollView(WrappedComponent) {
  return class Enhancer extends WrappedComponent {
    render() {
      const elementsTree = super.render();
      // We grab all the children components of the ScrollView
      const { children } = elementsTree.props;
      // And the original props passed to WrappedComponent element
      const { scrollY, ...rest } = this.props;
      return (
        <Animated.ScrollView
          {...rest}
          onScroll={Animated.event(
            [{ nativeEvent: { contentOffset: { y: scrollY } } }],
            {
              useNativeDriver: true,
            }
          )}
          scrollEventThrottle={1}
        >
          {children} /* We apply the children again here */
        </Animated.ScrollView>
      );
    }
  };
}

But wait, what if our WrappedComponent is a functional (or stateless) component instead of class based? Then the call to super.render() will break our application!

We need a bit of more work to take that case into account as well. Our final and complete implementation looks as below:

const isClassComponent = (Component) =>
  Boolean(Component.prototype && Component.prototype.isReactComponent);

function getNewElementTree(children, instanceProps) {
  const { scrollY, ...rest } = instanceProps;
  return (
    <Animated.ScrollView
      {...rest}
      onScroll={Animated.event(
        [{ nativeEvent: { contentOffset: { y: scrollY } } }],
        {
          useNativeDriver: true,
        }
      )}
      scrollEventThrottle={1}
    >
      {children}
    </Animated.ScrollView>
  );
}

function withAnimatedScrollView(WrappedComponent) {
  let renderTree;
  if (isClassComponent(WrappedComponent)) {
    return class Enhancer extends WrappedComponent {
      render() {
        renderTree = super.render();
        return getNewElementTree(renderTree.props.children, this.props);
      }
    };
  }
  // If WrappedComponent is functional, we extend from React.Component instead
  return class EnhancerFunctional extends React.Component {
    render() {
      // The below call is equivalent to super.render() in class based components
      renderTree = WrappedComponent(this.props);
      return getNewElementTree(renderTree.props.children, this.props);
    }
  };
}

Now we can finally apply this HOC to the cases where we want those scrollable components to be animatable as well.

const AnimScrollablePage = withAnimatedScrollView(ScrollablePage);

As a side note, you should consider the below points before putting into place this solution:

  • By calling render, you get a shallow representation of the tree, hence one level deep. That means you can’t access components that are deeply nested on the tree.
  • There may be a small performance hit by calling super.render repeatedly. You could optimise by caching the result and leveraging shouldComponentUpdate to determine when to re-render.

Summing up

We’ve ended up with just roughly 40 lines of code to encompass some sweet render hijacking that can be applied in the cases we need, without having to touch any of those 20 files, which would have caused incorporating and testing some extra ad-hoc logic for this particular requirement.

That means, better code maintainability, happier developers, happier myself.

This pattern may open the doors for other use cases you haven’t though of. It’s up to your imagination and creativity.

If you want to dive deeper into an exhaustive analysis of everything you can do with HOCs, I encourage you to check this amazing article.

If you found this article useful, follow me on Twitter @rgommezz for React Native updates and more content like this.

Happy coding! ❤️

Back to index

Raul Gomez • © 2024