Networking for real-device React Native testing on-the-go

3 minute read

As I build Meet Kinksters, one personal goal is to share the knowledge I develop along the way with others. So much of the details of building a complex app, especially with a native app client, are difficult to ordain without experience. So many of the tutorials I find online regarding the “tough problems” of decoupled web either paper over this complexity or offer only simplistic, not-production-ready solutions that omit important details like state management.

Real-device testing and the local network

One such sticky wicket has been how to handle networking when testing on a “real” device, in this case my Android phone. In an earlier blog post, I shared how I was able to use the Expo Go app with Dockerized build tools to get started with my mobile app. This helped get me going, but once I needed to implement and test in-app payments, I had to “graduate” to a development build. Furthermore, in-app payments are testable only on physical devices (at least for Android).

When running in an emulator, I was able to bind the Metro bundler and all my app services to ports on the Docker bridge interface. When I started testing on my physical device, I changed those bindings to, or all interfaces, and added a DNS record to my home (well, RV) router to resolve the docker-bridge hostname I’d been using to my laptop’s local network IP.

That’s well and good, but what happens when I’m off my local, trusted WiFi? I travel frequently, working from hotels and coffee-shops and libraries. Though it’s a relatively low risk, working similarly on these foreign networks means I’d have to make my configuration much more dynamic, if my devices could talk to one another to begin with. Then there’s the question of DNS resolution. There’s no real way to add custom host records in Android without rooting, and solutions like creating a Shadowsocks VPN from my phone to my laptop started feeling very janky and over-engineered.

Tethering to the rescue

After going down a few rabbit-holes (VPNs, custom DNS records/Unbound/Stubby, etc.) I remembered that my phone can do both Bluetooth and USB tethering, in addition to running a WiFi hotspot. What if I could make my laptop and phone share a private subnet, while allowing both to stay connected to the Internet over the hotel’s network? And keeping my VPN enabled, to boot?

The below solution works for both USB and Bluetooth tethering; Bluetooth seems a little slower, but it lets me skip a cable and doesn’t drop out when I bump the cable.

I’m on Pop! OS (basically, Ubuntu) but this should work on other OSes, too.

  1. Enable bluetooth or USB tethering on the device.
  2. Connect the laptop to the device, and edit the routing rules to enable “Use this connection only for resources on its network.” This will keep all your other traffic routed through your default route.
  3. Find the IP issued to the laptop from the phone. E.g.:
     $ ifconfig bnep0
     bnep0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
     inet  netmask  broadcast
  4. Configure Metro bundler, your app backend, and any other services your app needs over the network to bind to this IP. I want to avoid binding to all interfaces, including the hotel WiFi, but this also needs to be dynamic. So, I’ve changed my docker-compose.yml files to interpolate an environment variable. If I set it when running docker-compose up, that value is used. Otherwise, I have set a default for this environment variable in my .envrc file to the Docker bridge IP, as before.
        - "" # So I can always just do localhost:8082 for the web UI.
        - "${WEB_HOST_BASE}:8082:8080" # The IP for the tethered interface, or the Docker Bridge.
  5. Ensure the app also uses this environment variable in setting self-referential URLs, e.g. for CDN mocks, external services, etc.
  6. Start Expo and the app backend, setting the base IP to that of the tethered interface:
    $ WEB_HOST_BASE= docker-compose up

This not only allows me to securely test my app without exposing it to the WiFi network, but it enforces 12-Factor App discipline and removes hard-coded hostname strings from my codebase.