React Native + Webpack + HMR = ❤️

Authors
Paweł Trysła
Software Engineer
@
Callstack

Recently we added Hot Module Replacement support to Haul, a command line tool for developing React Native apps based on Webpack.

If you use the default template with <rte-code>index.ios.js<rte-code> or <rte-code>index.android.js<rte-code>, all you need to do is to add <rte-code>import 'haul/hot';<rte-code> at the top of the file so our babel plugin can add HMR logic for you. In other cases you need to manually set it up. Hopefully this post will help you with setup and shed some light on how it actually works under the hood.

How HMR works in Haul

The middleware and hot client

The middleware provides a way to send notifications to a running application whenever Webpack rebuilds the bundle.

middleware and hot client

The obvious choice was to use webpack-hot-middleware, however it uses EventSource to communicate with the client, which is not natively supported and I had a few issues with it, so I settled on WebSocket and rewrote the middleware and the hot client from the ground-up to use WebSockets. With that said, Haul hot client and middleware still use some parts of the original webpack-hot-middleware.

The environment

When the hot middleware sends a message, the client needs to download the hot update, which contains updated module implementation with calls to Webpack’s HMR API.

However, by default Webpack adds a new <rte-code><script><rte-code> tags for each update and rely on a browser to load the code.

But in the React Native environment we don’t have the DOM so the update fails.

code block with failed update

Fortunately, Webpack provides an option to change the templates with target property. Since there’s no react-native target, I used the webworker. It’s the closest to RN environment than any other, but there is a little catch.

HMR update check failed

The WebWorker target uses importScripts to load the hot update, which is available only in the WebWorker environment so we need to polyfill it. The native <rte-code>importScripts<rte-code> is synchronous, but the sync <rte-code>XMLHttpRequests<rte-code> are not supported in React Native, so the polyfill is actually asynchronous, and surprisingly it works well enough.

So now, the hot updates can be downloaded and the cache for modules updated with a new implementation, but we won’t have our components updated. Why? Because Webpack itself doesn’t know how to update them.

The HMR update logic

Webpack introduces the concept of accepting a module. Whenever the module is updated, Webpack HMR logic will trigger an event that will bubble up to the closest parent of that module which would need to accept the update to reflect the changes.

if (module.hot) {
module.hot.accept('./index.ios.js', () => {
// update logic
});
}
view raw gistfile1.txt hosted with ❤ by GitHub

It’s worth pointing out that the event bubbling is based on the modules tree. If module A imports module B, which imports module C and the module C changes, the event will firstly check if module C self-accepts, then if module B would accept the update, then it would check the module C if it either accepts a module C explicitly or implicitly by accepting the module B.

module tree

Usually we put acceptation logic in the top (root) file in our module tree and accepts its children.

In order to actually update our app, we need to provide some logic for it. I settled on react-hot-loader by Dan Abramov for this purpose. Let’s discuss how it works.

The babel plugin from react-hot-loader add a registration logic to a source, which takes a component and creates a proxy for it which is then stored in a cache. Next the patch module, which should be executed before any other code, patches <rte-code>React.createElement<rte-code> and <rte-code>React.createFactory<rte-code> to use components from cache, which means it will use the proxies.

A proxied component behaves exactly the same as a normal component with the exception being that it can be updated with a new implementation without losing it’s state.

Whenever the source file is changed, the component inside it will be re-registered and the proxy for it will be updated.

Finally, <rte-code>AppContainer<rte-code> is used to trigger deep update for the component tree and then the React itself handles the reconciliation and updates the UI.

However, with React Native we don’t render the app ourselves. We only tell which component to render using <rte-code>AppRegistry.registerComponent<rte-code>. The <rte-code>AppContainer<rte-code> from react-hot-loader is not the best suited for this approach.

So to make it easier, the <rte-code>makeHot<rte-code> function was born. Essentially it works like <rte-code>AppContainer<rte-code>, but it accepts a root component factory — <rte-code>() => MyApp<rte-code>  — and returns a new function with a new root component. This new component is used to trigger <rte-code>forceUpdate<rte-code> on the tree and contains the error handling. Then the <rte-code>redraw<rte-code> function must be called with a new root component factory to trigger the update.

/* other imports */
import MyApp from './MyApp';
AppRegistry.registerComponent('MyApp', makeHot(() => MyApp));
// later
if (module.hot) {
module.hot.accept('./MyApp', () => {
redraw(() => require('./MyApp').default);
});
}
view raw gistfile1.txt hosted with ❤ by GitHub

Both <rte-code>makeHot<rte-code> and <rte-code>redraw<rte-code> also accept a second argument with ID of the root component. This is useful if you use some navigation library like react-navigation or react-native-navigation with multiple root components.

/* other imports */
import App1 from './App1';
import App2 from './App2';
AppRegistry.registerComponent('App1', makeHot(() => App1, 'App1'));
AppRegistry.registerComponent('App2', makeHot(() => App2, 'App2'));
// later
if (module.hot) {
module.hot.accept('./App1', () => {
redraw(() => require('./App1').default, 'App1');
});
module.hot.accept('./App2', () => {
redraw(() => require('./App2').default, 'App2');
});
}
view raw gistfile1.txt hosted with ❤ by GitHub

Now we are able to re-render the App. But we need to add one tiny bit of code to clear the module in cache.

/* other imports */
import MyApp from './MyApp';
AppRegistry.registerComponent('MyApp', makeHot(() => MyApp));
// later
if (module.hot) {
module.hot.accept('./MyApp', () => {
clearCacheFor(require.resolve('./MyApp'));
redraw(() => require('./MyApp').default);
});
}
view raw gistfile1.txt hosted with ❤ by GitHub

Unfortunately, when you run the app it would crash.

app crash warning

This is because react-hot-loader will try to proxy all of the components, including the native ones like <rte-code>View<rte-code> or <rte-code>Text<rte-code>, which are not typical components, so they fail. To fix it, we need to exclude the native components and the solution I came up with is to re-patch those functions again. Then we can exclude native components and call original <rte-code>createElement<rte-code> or <rte-code>createFactory<rte-code>.

The re-patching logic is extracted to a separate module, which you need to import in an entry file so that it can run before anything else.

import 'haul/hot/patch'; // must come first
import { makeHot, redraw, clearCacheFor } 'haul/hot';
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
/* ... */
} from 'react-native';
/* ... */
view raw gistfile1.txt hosted with ❤ by GitHub

There’s also <rte-code>tryUpdateSelf<rte-code> function, which should be only used if your root component resides in the same file an <rte-code>AppRegistry.registerComponent<rte-code> call. This function tries to re-render all of the root components. Otherwise you would not see the updates to a root component.

Now we have a working HMR.

Welcome to React Native

Getting the “Enable Hot Reloading” button in dev menu to work

In order to get this button to have any effect, we need to update the hot middleware.

When you tap Enable hot replacement, React Native app will connect to a WebSocket under path <rte-code>/hot<rte-code>, and will disconnect, if you disable hot replacement. So, the middleware should send hot events only when the native hot client is connected to that WebSocket. With this solution, the middleware can also send <rte-code>update-start<rte-code> and <rte-code>update-done<rte-code> events to a native hot client, so you will see notifications at the top of the screen.

Here's more info about how the middleware manage those WebSockets.

Final words

Haul HMR should work with any navigation library, which uses a root component factory (<rte-code>() => MyApp<rte-code>). We’ve tested it with react-navigation and react-native-navigation. You can find the detailed guides on how to setup HMR with those libraries at Callstack GitHub.

Want to give it a try? Check out the Linaria project. Don’t forget to star it!

We’re still actively working on it, so please report any issues you find. Pull requests are always welcome.

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.