As you may have guessed from the number of friendly faces in the about section, scale is not a very big company. Nonetheless, we are able to maintain high development velocity because we are leveraging advanced tooling and automation. In this post, I will give some insight on our cross-platform toolchain by example of VaccTrack—an app I’ve built for iOS, Android, and web using only a single codebase.

Development setup with iOS, Android, and web

Requirements #

A quick overview of the platform itself: VaccTrack enables users to obtain proof of vaccination by means of universal vaccination certificates. These certificates are approved by the person giving the vaccination (using barcodes) and the certificate holder can verify their vaccination status with third parties for purposes like travel and business (using barcodes or signed PDFs).

VaccTrack Marketing Website

The product should be universally accessible and use platform features like the device camera (for scanning barcodes) and push notifications (to remind users of upcoming vaccinations). Thus, the decision was made to create mobile apps for iOS and Android, but also a web app to provide a lower barrier of entry, especially for low-engagement scenarios like certificate verification.

In order to save time and resources, it was clear that the iOS and Android apps will use cross-platform technology with a shared codebase (to be honest, I rarely recommend developing native native Android apps, given the effort it takes to support the complex ecosystem).

For the web app, I’d normally go ahead and develop a separate frontend that connects to the same API server as the apps and possibly shares one or two libraries. However, this time, I wanted to try something new ✨.

Two-in-one #

My go-to technology when it comes to cross-platform apps is React Native. With React already being a first-class citizen for creating user interfaces at scale (being used in roughly 80% of our projects), it’s a safe bet for maximum productivity.

React Native uses a hybrid approach to create apps for multiple platforms from one shared codebase; in this case, this means that it executes a JavaScript program to control Swift/Objective-C libraries (on iOS) or Java libraries (on Android), which in turn render platform-specific components (like text fields and navigation bars).

Let’s view an example; the following JavaScript (specifically, JSX) code renders an on/off switch:

<Switch value={true} />

When we run the program, we will see a beautiful switch component on both iOS and Android:

A beautiful switch component on iOS and Android

Note that the switch component on iOS looks a bit different from Android. This is because when running the program, React Native creates native components, which may look (and feel) (and behave!) differently according to the design guidelines of each platform. Not having to think about these differences—all while providing the behavior users expect from their operating system—is a huge advantage while developing products. On the other hand, it definitely helps to be somewhat familiar with each platform to understand what’s happening under the hood.

Turning it up to 3 #

For quite some time now, React Native supports web as a platform, and lately the project reached a level of maturity that made me want to give it a try.

Supporting three platforms with one codebase means that the level of abstraction has to be fairly high; the tooling has to “iron out” the differences between the platforms, shaving off parts that are impossible on the other platforms and filling in gaps that one platform is missing. Let’s consider our switch example from above; unlike iOS and Android, the web platform does not come with a switch component, so React Native Web just brings its own. This component may resemble the Android version, but it’s really a completely different implementation from the other platforms, all while still being rendered by the same snippet of code (<Switch value={true} />). What is this sorcery? 🧙‍♂️

A switch component on web

A big part of what makes our code work on multiple platforms lies in the way the React Native toolchain decides which files and functionality to include. One way is via platform-specific file names; let’s assume we want to have a component that should look and behave differently on each platform. Our folder structure could look something like this:

├─ MyCoolComponent.ios.jsx
└─ MyCoolComponent.web.jsx

When building the apps, React Native is smart and selects the file for the current environment, discarding the rest.

There are of course also ways of having different behavior at runtime:

import { Platform } from "react-native";

function getGreeting() {
  switch (Platform.OS) {
    case "android":
      return "Hello from Android";
    case "ios":
      return "Hello from iOS";
    case "web":
      return "Hello from web";

In order to reduce complexity and take full advantage of React Native, it helps to keep the use of platform-specific code to a minimum; having a lot of if…elses in your if…elses will increase your WTFs/min exponentially. In the whole VaccTrack app project I’ve only had to use platform-specific filenames twice: for the barcode scanner and for a signature input view (where the user signs a document with their finger). In both cases, the web version uses different implementations that should not be included in the native builds and vice versa.

Naturally, there are also third-party libraries that provide support for React Native Web and abstract away a lot of native/web differences, for example react-navigation for—you guessed it—navigation.

Summing up #

Looking back, I’m happy I gave React Native Web a spin. Being a web development professional, you can imagine my skepticism of tools that make it super easy to create websites. I’m very aware of the semantic and accessibility-related particularities of the web and I like to have control over what HTML tags get used and which styling gets applied.

The React Native Web tooling takes all this away from you. There is no CSS to speak of and HTML tags—while being available for web builds—do not work in native contexts. Instead of <div />, <section />, and all that goodness, there is only <View /> and—to add insult to injury—there is a common pitfall in that React Native does not support implicit text nodes:

// this crashes your app
<View>This is madness!</View>

// this is totally fine
  <Text>This is madness!</Text>

Nonetheless, the resulting web code is actually quite nice. Tabbing through input elements, scrolling, or mouse events all work as expected; regarding granular CSS control, advanced accessibility features and tight browser API integration, there is still a lot to be desired, but that’s part of the experience of using young technology.

After learning to work around the amicable quirks of the toolchain, I’m now quite happy with the productivity of being able to build three platforms with one codebase. It doesn’t even stop at three—there is currently a growing number of projects that add other platforms like Windows, MacOS, or tvOS to the mix. However, judging from the adventure that was the quest for the lowest common denominator of iOS, Android and web, I think, I’ll leave it at three for now 😅.

Any thoughts or comments?

Say hello on Twitter or contact us via email or contact form!