Cracking the Code: Hooking into React Native’s Hot Module Replacement(HMR)

Suson Thapa
Stackademic
Published in
6 min readMar 31, 2024

--

Photo by Volodymyr Hryshchenko on Unsplash

Hot Module Replacement(HMR) is an amazing technology that makes front-end development very efficient. This is a game changer when talking about mobile development.

Building mobile apps using native technologies is hard still to this day (although Jetpack Compose and SwiftUI are changing the landscape). If you want to change a text or color, you must rebuild the entire app, navigate to that screen, and verify the changes.

With React Native and HMR, you can view the changes simply by saving the file like this.

Although it is an amazing technology, we are not here to glorify HMR. Let’s talk about the problem I ran into and the solutions that I found.

Problem

I had a requirement to detect when the HMR reloaded the JS code. This can be useful to reset some native modules when the code changes.

I looked online and couldn’t find anything related to this. HMR was available to web developers for quite a long time(HMR was added to React Native in version 0.22). The very little content I could find on customizing HMR was related to web development(that mostly used WebPack and not Metro, the React Native bundler). But I didn’t give up and finally found what I was looking for. Let’s go through the solutions.

Solutions

Module.Hot

I was going through the Webpack documentation on HMR and found this. It looks like we can hook into the module.hot property to detect when it is replaced. This is what the code to detect HMR looks like for Webpack.

 if (module.hot) {
module.hot.accept('./file.js', function() {
console.log('Accepting the updated file module!');
})

I don’t know if this applies to Metro, but we can give it a try. Let’s see if we even have access to module.hot by console logging module.hot . This is the output.

{
"_acceptCallback":null,
"_didAccept":false,
"_disposeCallback":null,
"accept":[
"Function accept"
],
"dispose":[
"Function dispose"
]
}

Yay, it does have the accept function. Looking at the source code of Metro we see that the function doesn’t accept any file as an argument like Webpack.

type HotModuleReloadingCallback = () => void;
type HotModuleReloadingData = {|
_acceptCallback: ?HotModuleReloadingCallback,
_disposeCallback: ?HotModuleReloadingCallback,
_didAccept: boolean,
accept: (callback?: HotModuleReloadingCallback) => void,
dispose: (callback?: HotModuleReloadingCallback) => void,
|};

Let’s see what happens when we hook into the accept function with the following code in the App.tsx file.

if(module.hot) {
module.hot.accept(() => {
console.log('Module Hot Accept!');
})
}

It prints Module Hot Accept! when we change the file, this is good news. I’m still unsure why the Metro’s module.hot.accept doesn’t take a file as an argument like Webpack. Let’s see what happens when we have the listener set up in App.tsx file but change the code in Section.tsx file.

This is exactly what I expected. If you want to detect the changes in Section.tsx file you will have to attach the listener in there as well. If you just want to detect when a single file gets reloaded you can use this approach.

If you want to detect the file changes globally without adding listeners all over the place, sit tight, it is going to be a bumpy ride.

React Native’s HMR

I know React Native somehow detects the file changes under the hood to reload the app. So, it’s time to get our hands dirty. Looking through the node_modules I saw this beautiful class called HMRClient which has some useful code.

It looks like there is a WebSocket connection going on and it is listening to various message types like update-start , update , update-end , etc.

Looking more into the source code I found another file named HMRClient under the React Native package.

Let’s try to understand the code. It creates the Metro’s HMRClient and hooks into the events emitted by the client. It is kinda amazing to see that the “Refreshing…” message we get when doing a hot reload is actually set here. If I want, I can change it to anything but we are not interested in that.

At this point, we can patch the React Native to expose the HMRClient and hook into the events ourselves. But I don’t like the idea of patching React Native and having to maintain it. Let’s dig even deeper.

We know that the HMRClient uses WebSocket so let’s see if there is a native implementation of it on Android. Sure there is.

The native side emits websocketMessage event whenever it receives some data. Let’s try hooking into it using the DeviceEventEmitter on the JS side.

DeviceEventEmitter.addListener('websocketMessage', (data) => {
console.log(`WebSocketMessage: `, data);
})

This is the output.

 LOG  WebSocketMessage:  {"data": "{\"type\":\"update-start\",\"body\":{\"isInitialUpdate\":true}}", "id": 1, "type": "text"}
LOG WebSocketMessage: {"data": "{\"type\":\"update\",\"body\":{\"revisionId\":\"a7cad0bd112dc499\",\"isInitialUpdate\":true,\"added\":[],\"modified\":[],\"deleted\":[]}}", "id": 1, "type": "text"}
LOG WebSocketMessage: {"data": "{\"type\":\"update-done\"}", "id": 1, "type": "text"}
LOG WebSocketMessage: {"data": "{\"type\":\"bundle-registered\"}", "id": 1, "type": "text"}

Yay, we are getting the events for HMR.

It emits a lot of events when you make even a single change so we will have to do some sort of filtering. Here is the final code that I came up with to detect when the HMR replaces the JS files.

function setupHMRListener() {
if (!__DEV__) {
return
}
let isUpdating = false
DeviceEventEmitter.addListener('websocketMessage', (data) => {
try {
const response = JSON.parse(data.data)
if (response.type === "update" && response.body?.modified?.length > 0) {
isUpdating = true
}

if (isUpdating && response?.type === "update-done") {
isUpdating = false
// Do your changes here!
}

} catch (e) {
console.log('Failed to parse the webSocketMessage event data!', e)
}

})
}

And it works great.

Please keep in mind that you might get multiple updates for a single change so make sure to implement debouncer if needed.

Finally, I wasn’t sure about iOS so I quickly tested it on iOS, and it works great.

For those of you who made it to the end, here is a link to the code.

Well, this concludes today’s blog. See you in the next one, peace!

Stackademic 🎓

Thank you for reading until the end. Before you go:

--

--

Android | iOS | Flutter | ReactNative — Passionate Software Engineer