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.
Here’s the jist
- A NodeJS server, called Packager, is started up in a terminal.
- 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 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?
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:
The end result of running npm start (or just hitting run in Xcode) is a terminal like so:
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.
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.
Step 3: Turn on Debugging Mode
We turn on debugging mode by typing Command+D on the device and clicking the “Debug in Chrome” option.
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
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
I hope this helped you better understand how debugging works for React Native applications.
That’s it for now.
Appendix A: ApplicationScript Injections