Sweet Render Hijacking With React

Authors
Raul Gomez Acuna
Software Engineer
@
Callstack

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:

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(),
};
...
render() {
const newProps = {
isLoggedIn: this.state.isLoggedIn,
}
return <WrappedComponent {...this.props} {...newProps}/>
}
}
}
view raw hoc.js hosted with ❤ by GitHub

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:

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 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.

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

‍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.

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 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.

Insights

Learn more about Developer Experience

cover

May 5Article

Codex Ambassador: Q1 Recap Across 5 Cities

A short recap of Codex events in Berlin, Wroclaw, Warsaw, Sopot, and Miami, plus what we learned from each format and where we want to go next.

cover

Feb 5Live Stream

Storybook Explained: From Basics to Advanced Workflows

Core Storybook concepts, stories and args, interaction testing, and how Storybook works in React Native projects.

cover

Jan 29Article

Announcing AI SDK Profiler for React Native

Announcing the AI SDK Profiler plugin for Rozenite DevTools. Capture OpenTelemetry spans from the Vercel AI SDK and inspect AI requests across on-device and cloud providers.

cover

Nov 28Talk

How I Hacked React Native DevTools (And Built a Plugin Framework)

Learn how React Native DevTools work under the hood, and how to inject custom plugins with clean hooks and no forks.

cover

Sep 16Talk

How I Hacked React Native DevTools (And Built a Plugin Framework)

Learn how React Native DevTools work under the hood, and how to inject custom plugins with clean hooks and no forks.

cover

Jul 28Article

Introducing Rozenite: A Plugin Framework for React Native DevTools

Build custom debugging panels inside React Native DevTools with Rozenite: a modular, TypeScript-ready plugin framework with full device-scoped support.

cover

Feb 12Article

How to Easily Create Native Libraries With Nitro and Turbo Modules

Learn how to optimize React Native performance with Turbo and Nitro Modules. Ditch the legacy bridge and master modern native module development.

cover

Dec 4Podcast

Radon IDE: Better Developer Experience for React Native Teams

Find out how Radon IDE can help increase team velocity and rid you of the common React Native app development pain points.

cover

Sep 19Podcast

Exploring Bun: Jarred Sumner on Hyperfast JavaScript Toolkit

Is Bun the future of React Native package management? Join us as we talk with Jarred Sumner, the creator of this lightning-fast JavaScript toolkit.

cover

Sep 11Podcast

Vim: from Zero to Hero

Discover the power of Vim, the legendary text editor, as we explore its origins, standout features, and practical tips to boost your coding efficiency.

cover

May 15Article

Add-Ons for Web Browsers With React

At times you may need to create an add-on to use a web browser more effectively. Find out how to build one with React.

cover

Jan 27Article

Why Is It Worth to Automate Your Dependency Management with `Autolinking`?

When you automate your dependency management, you can fully focus on developing your app. Find out how you can do it with autolinking.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.