Deep Diving React Native Debugging

If you’re like me, when you saw how easy debugging React Native applications was, you were mind-blown. Setting breakpoints in Google Chrome to control your iOS device seemed like magic and sorcery.

But if you’re like me, you also distrust magic in programming. Magic must be understood so that it turns into engineering fact & competence. This article attempts to demystify React Native Debugging so that you understand the technical details behind it.

tl;dr

Here’s the jist

  • A NodeJS server, called Packager, is started up in a terminal.
  • Google Chrome opens up and loads your React Native JavaScript from Packager as a normal <script> tag. Note that the React Native application now sits in the browser with full debugging support.
  • The device communicates with the application running in the browser via WebSockets proxied through Packager. JSON commands are set back and forth between the device and the browser, with the browser instructing the device as to what it should do.

That’s it. If you understand it all now, then you’re done!

The Deep Dive

If you’ve continued on, then you’re more interested in learning the exact processes, files and operations that allow for debugging.

Note: React Native is rapidly changing. This article references React Native 0.11.2

Let's start out deep dive by introducing the actors at play

The various actors in a typical React Native application

The map for React Native debugging is a bit complex. I know that Facebook is working on Nuclide IDE to streamline all this, regardless though, it's still best to be able to see the independent characters.

A. Your mobile device: This is where the native code is running.

B. NodeJS server: A NodeJS Server running Facebook’s Packager (link) project. A WebPack like project that provides a CommonJS module like system + a lot of other kitchen sink functionality for React Native development

C. Google Chrome: My favorite Web Browser. Yours?

D. React Native JavaScript code: Your JavaScript code


So Tell Me… What are the Steps to Make Debugging Happen?

Step 1: Start Packager

It all starts with “npm start”. By issuing this command we run the small bash script packager.sh which essentially just runs packager.js. Packager.js starts a NodeJS server using Sencha Labs’ Connect Server.

which listens to several routes on localhost. The routes we’re going to be working with today are:

http://localhost:8081/index.ios.bundle

http://localhost:8081/debugger-ui.html

http://localhost:8081/launch-chrome-devtools

The end result of running npm start (or just hitting run in Xcode) is a terminal like so:

Typical terminal output for Packager in GREEEEEN!

Packager also sets up a WebSocket proxy, which we’ll get to later.

Step 2: Run React Native App in Simulator

The next step is to run the application in a simulator. Using Xcode, we compile and run the Objective-C/Swift portion of React Native code in a simulator.

The default React Native application running on an iPhone 5S (iOS 9.0) simulator

Once the binary is running on the device, one of its first instructions it to reach out to http://localhost.com:8081/index.ios.bundle and grab the transpiled React Native application.

Remember, that Packager is running on localhost, so once it receives the request, it will use Babel and Facebook’s Haste dependency grapher to collect, concat, transpile and modularize all of your React Native JavaScript files into a single response.

Try it: navigate your browser to http://localhost:8081/index.ios.bundle and see your JavaScript application as React Native does.

With all the JavaScript code on the device, we now put the JavaScript code into Apple’s JavaScriptCore and run the application as normal.

Step 3: Turn on Debugging Mode

With the application running normally on the phone, our next step is to activate debugging mode. Debugging mode is good for, well, debugging your JavaScript (duh), but it's also nice in that you can use Google Chrome’s WebSocket inspector to see the traffic being communicated between React Native the compiled part and React Native the JavaScript part.

We turn on debugging mode by typing Command+D on the device and clicking the “Debug in Chrome” option.

React Native’s on-screen Developer menu

The resulting action is an HTTP request to http://localhost:8081/launch-chrome-devtools. This request is received by Packager, which in turn runs an AppleScript file “launchChromeDevTools.applescript”. The AppleScript performs two actions.

NOTE 01.14.16: The AppleScript file has been replaces by npm package opn.

First, the AppleScript opens a new tab in Google Chrome. Secondly, the AppleScript file activates the new tab in Chrome and instructs it to load the url http://localhost:8081/debugger-ui.html, which maps to the static file debugger-ui.html.

Step 4: Debugger-ui.html and Device establish a connection

Now that we have an html file loaded into Google Chrome, the first the thing the webpage does is to establish a WebSocket connection back to Packager and wait for a WebSocket ping from the device.

The device will send 3 WebSocket pings out to the browser. If the browser responds back with the expectedId/sessionId, then the connection is deemed established and execution commences normally. Otherwise the device will throw up a read screen asserting that the “Runtime is not ready”.

Step 5: Execute Application Script

With the connection now established, the device will send a WebSocket message to the browser to load the application script.

The message it sends is interesting, in that there is an inject key on the dictionary has a large module/method/id mapping (see Appendix A. for an example output).

This message is then received by debugger-ui.html running in Chrome. The message is unpacked and the information under key “inject” are attached to the window object under namespace “__fbBatchedBridgeConfig”.

Try it: When running in debug mode on Google Chrome type window.__fbBatchedBridgeConfig in Chrome’s console to inspect the module/method/id mappings

After the window environment has been updated with the contents of the inject key, the function “loadScript” is executed. The loadScript function creates a new <script> DOM element. The <script> tag’s src element points to http://localhost:8081/index.ios.bundle which causes the React Native JavaScript file to be downloaded and run in the browser.

At the end of this process, the onload action for the <script> tag is called and the reply to the executeApplicationScript WebSocket message is sent.

Step 6: Run

With the “executeApplicationScript” response successfully received back on the device, the React Native application will continue as it normally would had the JavaScript been loaded locally.

But now we are communicating with the JavaScript sitting on a Google Chrome browser via WebSockets. Crazy!

And since the JavaScript is running as any other JavaScript would in Google Chrome, we have full debugging support with breakpoints, watch variables, runtime inspection, etc, etc.

Conclusion

What’s nice is that the same process for running React Native in Google Chrome is nearly the same process for running React Native on a JavaScriptCore.

For example, the same process to inject “__fbBatchedBridgeConfig” variables in the browser, is the same process that happens on the device. This similarity is great, because we now have a way to see what’s happening inside JavaScriptCore as well as inspect the messages go in and out of it.

I hope this helped you better understand how debugging works for React Native applications.

That’s it for now.

Appendix A: ApplicationScript Injections


End