Sweet Render Hijacking With React
In short
The article introduces the Inheritance Inversion HOC, an unconventional pattern allowing access to the render tree of a WrappedComponent before React's reconciliation process. By returning a class component that extends WrappedComponent, the HOC gains access to state, props, and the render method. This approach offers flexibility without directly modifying 20 component files, enhancing code maintainability and developer satisfaction. Considerations include shallow tree representation and potential minor performance impacts.
I’d like to share with you some obscure patterns 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).
What are 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:
Let’s take a look at a possible implementation for abstracting state, which could be sharing some authentication state needed by several components:
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 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:
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 WrappedComponentfor the ref to be calculated, that means that you need to return the WrappedComponent element from the HOC render method, let React do it’s 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.
This pattern allows the HOC to have access to the WrappedComponentinstance 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.
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:
Now we can finally apply this HOC to the cases where we want those scrollable components to be animatable as well.
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 optimize by caching the result and leveraging shouldComponentUpdate to determine when to re-render.
Summary
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 considered. It’s up to your imagination and creativity.
You can dive deeper into an exhaustive analysis of everything you can do with HOCs. Enjoy the dive! It's an amazing read.