Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Pirsanth/9d7fd53a5b6f64a8f6d84f07d7e62c01 to your computer and use it in GitHub Desktop.
Save Pirsanth/9d7fd53a5b6f64a8f6d84f07d7e62c01 to your computer and use it in GitHub Desktop.
How to run 2 different react native apps on 2 different emulators/devices at the same time

How to run 2 different react native apps on 2 different emulators/devices at the same time

I've been a react-native developer for about one and a half years now. I have only recently begun reading some of react-native-community/cli's source code in preparation of becoming an open source contributor. In the process of doing so, I have found ways of alleviating a few common pain points for me when developing with react-native. This gist will deal with one such pain point, as per the title above and is a part of a larger series of such posts.

Story time

Click here to view a higher resolution version of the above gif (Github's Camo only allows gifs smaller than 5 megabytes)

For a project at my last company, we has two separate apps one that was the admin version of the app and another that was the client version of the app with two seperate repos. Both versions of the app worked together for example, the client can make a request for a table via the client app and the admin can view all requests for a table in the admin app. It would've been useful then to develop both react-native apps at the same time to be able to change the JS code of either app on the fly to view the log of a specific variable or change the body of a http post request, for example.

A two part solution to the problem

There are two parts to tackling this problem:

  1. We need the metro server to run on a port number of our choice.

  2. We need to make sure the react-native application loads the js bundle from the metro server with the aforementioned port number and we have to make sure the react-native cli installs and launches the app on a specific mobile device/emulator.

If we can do this for one such device/emulator and port number combination we can simply do so again with another (different) port number and device/emulator combination. This would thus solve the problem of running 2 different react-native apps at the same time on 2 different emulators/devices.

Tackling the first part of the problem is quite straightforward. To make metro run on a port of our choice we simply use npm start -- --port portNumber. Where portNumber is the portNumber we choose. Note: I strongly recommend running npm start -- --port portNumber to start the metro server rather than letting the command react-native run-android --port portNumber or react-native run-ios --port portNumber start the metro server instead. The reason why is because the process of the react-native run-android or react-native run-ios command starting a metro server differs by OS and the by terminal application used to run the command (on a Mac react-native run-android opens a new tab with the same terminal application used to run react-native run-android whereas react-native run-ios always opens the metro server with the Terminal app).

Tackling the second part of the problem is much more involved and is detailed below for react-native run-android and react-native run-ios respectively.

Making react-native run-android's resulting app communicate with metro on a specific port and making react-native run-android install and launch the application on a specific device

Example commands: npm start -- --port 7777 npx react-native run-android --port 7777 --deviceId "emulator-5554"

Note: you can get the device id using the adb devices command in a terminal

Why does the example command not work?

If we run npx react-native run-android --port 7777 (note: its without the deviceId) we see that it it installs the react-native app on all connected devices (bad) and uses the metro server on port 7777 (good). However when we specify a deviceId to the above command like so: npx react-native run-android --port 7777 --deviceId "emulator-5554", it installs the app on the correct device but does not use the passed in metro port argument of 7777 (it uses the default port 8081).

The above occurs because when npx react-native run-android --port 7777 is ran, the relevant gradle command is ./gradlew app:installDebug -PreactNativeDevServerPort=7777 on Mac 5 which passes the port argument (7777 in this case) as a gradle project property via the command line. When the above command is ran with a deviceId as in the example command npx react-native run-android --port 7777 --deviceId "emulator-5554", the corresponding gradle command is ./gradlew build -x lint instead (on Mac as well) 6 which does not pass a gradle project property via the command line 4.

Note: The arguments passed to the gradle wrapper are the same for Mac and Windows so even though I have not tried this on a Windows machine, the above observations should still hold for Windows

How do we make the example command work?

Solution 1: In app’s build.gradle (./android/app/build.gradle) add project.ext.reactNativeDevServerPort with the metro server's port number:
    project.ext.react = [
    enableHermes: false,  // clean and rebuild if changing
    ]

    project.ext.reactNativeDevServerPort = "7777"
    //Add the above line with your metro server's port number 
    //In the example command above, we started the metro server on port 7777
Why does it work?

By passing gradle the reactNativeDevServerPort project property, gradle will assign the value 7777 to R.integer.react_native_dev_server_port 7. React-native's Java source code then uses R.integer.react_native_dev_server_port to get the metro server's port number 8. The ip address of the metro server is determined by the function getServerIpAddress 9 and it depends on where the react native app is running (more details are given below). The full url the react native app uses to reach the metro server is of the form serverIpAddress:metroPort where serverIpAddress is determined by 9 and metroPort is determned by 8 with a default value of 8801 if the reactNativeDevServerPort project property was not passed in 14.

Keeping how the metro server's url is determined in mind, here's how it works for all three kinds of devices, given our example commands above using the first solution:

  1. AVD Android Emulator The url used by the emulator is 10.0.2.2:7777. 10.0.2.2 is a special ip address that has been set up by the AVD Android team to access the computer's localhost. So the above url used inside an emulator will query the computer's localhost at port 7777
  2. Genymotion Emulator The url used by the emulator is 10.0.3.2:7777. Similar to the above, 10.0.3.2 is a special ip address that has been set up by the Genymotion team to access the computer's localhost. So the above url used inside the Genymotion emulator will query the computer's localhost at port 7777
  3. A physical android device Here's where it gets interesting, the url used here is localhost:7777. The localhost here refers to the android device's localhost NOT the computer's localhost. When the npx react-native run-android --port 7777 --deviceId "emulator-5554" example command is ran the line 10 runs adb -s emulator-5554 reverse tcp:7777 tcp:7777for our given example command above. What this does, in this case, is make localhost:7777 from inside the android device lead to the computer's localhost:7777. Hence, the physical android device can communicate with the metro server at port 7777.
Solution 2: A physical android device only solution

This solution does not modify any files unlike solution 1 above which add lines to app module's build gradle file. Instead, before running the example commands above run adb -s yourDeviceId reverse tcp:8081 tcp:yourMetroServerPort first, substituting values for yourDeviceId and yourMetroServerPort. This command thus becomes adb -s "emulator-5554" reverse tcp:8081 tcp:7777 for our example commands above. After running that I run the example commands and they work.

Why does it work?

Since the reactNativeDevServerPort gradle project property was not passed in, the default port number of 8081 is used. The metro url used by react-native will be localhost:8081 from the physical android device. What the adb reverse command above does is make localhost:8081 from the physical android device acess the computer's localhost:7777.

This solution only works for physical android devices because physical android devices use localhost for the ip address which refer's to the physical android device's own loopback service whereas the emulators use a special ip address which accesses the computer's localhost directly. That's why this solution does not work for the emulators.

Making react-native run-ios's resulting app communicate with metro on a specific port and making react-native run-ios install and launch the application on a specific device

Example commands: npm start -- --port 7777 npx react-native run-ios --port 7777 --simulator "iPhone 12"

Why does the example command not work?

Running the above commands we see that the react-native app is still trying to access the metro server on the default port of 8801.

When we run npx react-native run-ios --port 7777 --simulator "iPhone 12", xcodebuild is ran with the RCT_METRO_PORT environment variable set to the port argument we passed to react-native run-ios (7777 in our case)13.

In RCTDefines.h we have 11.

    #ifndef RCT_METRO_PORT
    #define RCT_METRO_PORT 8081
    #else
    // test if RCT_METRO_PORT is empty
    #define RCT_METRO_PORT_DO_EXPAND(VAL) VAL##1
    #define RCT_METRO_PORT_EXPAND(VAL) RCT_METRO_PORT_DO_EXPAND(VAL)
    #if !defined(RCT_METRO_PORT) || (RCT_METRO_PORT_EXPAND(RCT_METRO_PORT) == 1)
    // Only here if RCT_METRO_PORT is not defined
    // OR RCT_METRO_PORT is the empty string
    #undef RCT_METRO_PORT
    #define RCT_METRO_PORT 8081
    #endif
    #endif

What the above block of code does is set the value of RCT_METRO_PORT to the default port number of 8801 if the RCT_METRO_PORT preprocessor macro is undefined or is an invalid value.

In RCTBundleURLProvider.mm we have const NSUInteger kRCTBundleURLProviderDefaultPort = RCT_METRO_PORT; 12 which uses the imported value of RCT_METRO_PORT from RCTDefines.h for the port of the metro server.

Our problem is that the RCT_METRO_PORT environment variable is not set as the RCT_METRO_PORT preprocessor macro.

How do we make the example command work?

Solution:

What we are going to do now is add a RCT_METRO_PORT preprocessor macro (that will take the value of the RCT_METRO_PORT environment variable) to the build settings.

  1. First double click the workspace file (the white file NOT the blue file) under the ios directory of your react native project and it will open in xcode.

  1. Then in the resulting xcode window, click on the icons and fill in the fields in the order suggested by the picture below (alphabetically)

  A) In the left most pane click on the label with the blue Pods file
  B) In the inner left pane click on the blue Pods file under Project
  C) Click on the Build Settings tab
  D) In the search bar type in `Preprocessor` to narrow down the results
  E) Double click on the preprocessor macros under the debug build configuration for Pods. 
     In the resulting popup, click on the plus icon to add a preprocessor macro line. 
     Add the line RCT_METRO_PORT=$(RCT_METRO_PORT)
Why does it work?

The line RCT_METRO_PORT=$(RCT_METRO_PORT) sets the RCT_METRO_PORT preprocessor macro to equal the RCT_METRO_PORT environment variable that is set when xcodebuild is ran 13. The RCT_METRO_PORT environment variable is, in turn, equal to the port argument passed to npx react-native run-ios --port 7777 --simulator "iPhone 12" (7777).

Note: If the bundle url still cannot be loaded try Product > Clean Build Folder in xcode then run the react-native run-ios command. I suspect its because the app has to be rebuilt from scratch for the port number passed to the C preprocessor to have any effect


References

  1. The react-native-community/cli project

  2. How to pass port arguments to npm start

  3. Explaining how abd reverse works

  4. How to pass project properties to gradle via the command line

  5. How when we run npx react-native run-android --port 7777 (without a deviceId) it passes the port argument (7777) as a gradle project property

  6. How when we run npx react-native run-android --port 7777 --deviceId "emulator-5554" (with a deviceId) the port argument is not passed as a gradle project property via the command line

  7. How gradle assigns the reactNativeDevServerPort gradle project property to R.integer.react_native_dev_server_port (reactNativeDevServerPort can be passed in the command line or it can be defined in app's build.gradle file)

  8. How the react-native Java source code uses R.integer.react_native_dev_server_port to get the metro server's port number

  9. How the react-native Java source code gets the metro server's ip address

  10. The line in the react-native cli code that runs adb -s emulator-5554 reverse tcp:7777 tcp:7777 in the case of our example command above

  11. How the default metro server port of 8801 is used if the RCT_METRO_PORT preprocessor macro is undefined or is an invalid value

  12. The code that uses the value of RCT_METRO_PORT from RCTDefines.h for the metro server port. The resulting http requests will be of the form localhost:RCT_METRO_PORT

  13. How when the xcodebuild command is ran, the port argument passed to react-native run-ios is set as an environment variable, RCT_METRO_PORT

  14. How the default dev server port number is set for Android if the reactNativeDevServerPort project property is not passed in

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment