Debugging React Native iOS Apps with Safari

React Native has great developer experience, and the debugging workflow is at the core of the workflows. Today, we use Chrome to debug JavaScript in React Native apps. In this blog, I explore an alternative approach to the debugging workflow using Safari.

How debugging works today

To start debugging, we open the developer menu (using Cmd+D) and select "Debug with Chrome". This action tells the Metro Packager to open the Chrome browser and the JavaScript code now runs on Chrome, instead of running on the device. A web socket connection between Chrome and the device/emulator via metro is responsible delivering the JS commands to Chrome, and sending the results of the statements back to the emulator.

Issues with Chrome as a Debugger

The entire debugger setup is pretty clever as it brings the classic web development workflow to mobile. However, there are a few issues with this setup
  1. The JS VM running on the device is JavaScript Core (JSC), and is different from V8/Chrome during debug mode. These JS VM differences could lead to hard to fix bugs.
  2. Communication during debug mode is over a web socket, which is inherently asynchronous. This poses a problem for cases when native modules expose synchronous APIs. The issue only gets larger Fabric and TurboModules, which have many more synchronous methods.
  3. Many developers currently using the JS Profiler in Chrome to understand the  performance of their React Native app. The profiles are not accurate since they have that web socket layer, introducing a level of network latency, which is not present in the real app.  

Using Safari

Turns out, Safari can be used to connect to apps that run JSC. For React Native apps, we can use Safari to debug JavaScript and use workflows like setting breakpoints, inspecting variables, etc. However, the JSC debugger in Safari does not use the network stack, which means that the sourcemap URL in the index.bundle file is not evaluated, leaving us to debug one giant JS file.
To work around this, we an use inline sourcemap that can be enabled by a single line change in the Appdelegate.m file.

In the file sourceURLForBridge, replace return [[RCTBundleURLProvider sharedSettings]...  with the following

[NSURL URLWithString:[[[[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil] absoluteString] stringByAppendingString:@"&inlineSourceMap=true" ]];

This line simply appends a inlineSourceMap=true, making metro return the sourecmaps with the index.bundle file.
Once this is setup, Safari can successfully recognize the sourcecmaps and open the source files correctly.



Next Steps


Note that this method works with emulators, it does not work with the device. Now that we have Safari working, I would also like to get the JS Profiler in Safari, to give us accurate JS execution information, which should help improving performance of the apps. 

Using Flipper with React Native

Flipper is a great developer tool for debugging mobile apps and quite popular in the Android and iOS communities. As a companion app for the mobile app development process, it provides tools to inspect network traffic and understand the native UI view hierarchy. With its extensible plugin API, Flipper is now an indispensable tool for many developers at Facebook who work on the family of mobile apps.

Using Flipper with existing React Native Apps today

If you are using a version of React Native lesser than 0.62, you would have to follow these steps to enable Flipper. These steps are applicable to both existing brownfield and greenfield React Native projects, and projects created from the command line.
This is mostly due to conflicting versions of Yoga. React Native ships Yoga as a part of the repo, while Flipper picks it up from gradle/cocoapods.
Note that these issues have been resolved in the master branch.


The code in the video is also available in a commit that can be used as a reference for integration. 

Android

Assuming <your_rn_app> is the root of your react native project, 
  • Create a new file called in <your_rn_app>/android/src/debug/java/com/your/package/name/ReactNativeFlipper.java and copy the contents from here. This class is responsible for initializing and adding all the plugins needed. This file is added to the "debug" folder since we don't want Flipper to be packaged in our production app.
  • In <your_rn_app>/android/src/main/java/com/your/package/name/MainApplication.java
    • Add this new function and its imports. This function uses reflection to ensure that Flipper is not bundled in the production copy of your app.
    • In the onCreate method, add initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); to start Flipper.
  • In <your_rn_app>/android/app/build.gradle, inside dependencies section, add these lines for including Flipper in the project. Note that in the dependencies, we exclude some dependencies like Yoga since we want to use the version of Yoga that is packaged with React Native.
Open the Flipper Desktop Client and you should be able to see your app, with plugins like native UI hierarchy manager, Databases, Network requests, Shared Preferences and Images.

iOS

The  iOS integration is a little tricky since there is no simple way to exclude Yoga from cocoapods. I have fixed this issue using two diffs (1, 2).
  • Patch React Native with my changes by running this gist of find/replace of shell commands.
  • Make the changes from this commit to the <your_rn_app>/ios/Podfile and <your_rn_app>/ios/app_name/AppDelegate.m
  • Run pod install to install the additional cocoapods for flipper
  • Run the app
Open the Flipper Desktop Client, and you should be able to see the plugins for Layout, Network and User preferences.

You can also check out the sample RNTester app that has flipper integration and could use the commits for Android and iOS as examples for integrating.

Flipper by Default

Fixes for the conflicts due to different versions of Yoga are solved and are in master. This means that the new templates come with files like ReactNativeFlipper.java.

Hence, for versions of React Native >= 0.62
  • For Android, simply add initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); to MainApplication.java's onCreate method.
  • For iOS, add [AppDelegate initializeFlipper:application]; as the first line in AppDelegate.m's didFinishLaunchingWithOptions method.
If you are using a brownfield app with newer versions of React Native, these steps will still apply if you have updated your project with the latest template.
Alternatively, you can use react native from source and update your package's dependency of react-native to point to github:facebook/react-native#master instead of a specific version. 


Next Steps

  1. Create a plugin that can help visualize React Performance. similar to the diagram from a previous conference talk.    
  2. Work with the community on adding more plugins (like redux, redux-saga, mobx, etc.) to make the React Native developer experience even better !!