Networking for real-device React Native testing on-the-go
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 0.0.0.0
, 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.
- Enable bluetooth or USB tethering on the device.
- 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.
- Find the IP issued to the laptop from the phone. E.g.:
$ ifconfig bnep0 bnep0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.44.226 netmask 255.255.255.0 broadcast 192.168.44.255
- 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 runningdocker-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.services: web: ports: - "127.0.0.1:8082:8080" # 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.
- Ensure the app also uses this environment variable in setting self-referential URLs, e.g. for CDN mocks, external services, etc.
- Start Expo and the app backend, setting the base IP to that of the tethered interface:
$ WEB_HOST_BASE=192.168.44.226 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.