React Native + Webpack + HMR = ❤️

Paweł Trysła
Callstack Engineers
7 min readSep 26, 2017

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 index.ios.js or index.android.js, then all you need to do is to add import 'haul/hot'; at the top of the file, so our babel plugin will 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.

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 <script> tags for each update and rely on a browser to load the code,.

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

Fortunately, Webpack provides an option to change the templates with target property (https://webpack.js.org/configuration/target/). 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.

The WebWorker target uses importScripts to load the hot update, which is available only in WebWorker environment, so we need to polyfill it. The native importScripts is synchronous, but the sync XMLHttpRequests 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
});
}

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.

Usually we put acceptation logic in the top (root) file in our module tree and accepts it’s 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 React.createElement and React.createFactory 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, AppContainer 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 AppRegistry.registerComponent. The AppContainer from react-hot-loader is not the best suited for this approach.

So to make it easier, the makeHot function was born. Essentially it works like AppContainer, but it accepts a root component factory — () => MyApp — and returns a new function with a new root component. This new component is used to trigger forceUpdate on the tree and contains the error handling. Then the redraw 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));// laterif (module.hot) {
module.hot.accept('./MyApp', () => {
redraw(() => require('./MyApp').default);
});
}

Both makeHot and redraw 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'));
// laterif (module.hot) {
module.hot.accept('./App1', () => {
redraw(() => require('./App1').default, 'App1');
});

module.hot.accept('./App2', () => {
redraw(() => require('./App2').default, 'App2');
});
}

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));// laterif (module.hot) {
module.hot.accept('./MyApp', () => {
clearCacheFor(require.resolve('./MyApp'));
redraw(() => require('./MyApp').default);
});
}

Unfortunately, when you run the app it would crash.

This is because react-hot-loader will try to proxy all of the components, including the native ones like View or Text, which are not a 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 createElement or createFactory.

The re-patching logic is extracted to a separate module, which you need to import in 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';
/* ... */

There’s also tryUpdateSelf function, which should be only used if your root component resides in the same file a AppRegistry.registerComponent 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.

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 /hot, 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 update-start and update-done events to a native hot client, so you will see a notifications at the top of the screen.

If you’re interested in how the middleware manage those WebSockets, check out this file: https://github.com/callstack-io/haul/blob/master/src/server/middleware/hotMiddleware.js

Final words

Haul HMR should work with any navigation library, which uses root component factory (() => MyApp). 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 here: https://github.com/callstack-io/haul/blob/master/docs/hmr/Setup.md#manual-setup

Want to give it a try? Check it out at https://github.com/callstack-io/haul/. 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.

--

--