
“Why can’t we just write this code once and have it just work across Web, Android and iOS?” As a mobile engineer who has spent both a significant portion of his career copying the exact same product from iOS to Android, I’ve asked myself this question many times.
At Ambrook, we’re lucky to have the opportunity to approach the challenge of supporting multiple platforms without the constraints of legacy engineering decisions. As a result, we’ve been able to share code across web and mobile, allowing all of our entire engineering team (currently four engineers) to contribute with little overhead.
Ambrook is building a suite of financial tools for farmers that needs to be consistent and fully-featured across desktop and mobile. So that farmers can use our tools in the field, they must work well on slower internet connections and while offline. As an early stage company, we are prioritizing rapid delivery of new features as we get feedback from our users.
When I first joined the Ambrook team, I was tasked with figuring out how to translate our existing web app to mobile. The web app was written in Typescript using React DOM and Next.js, which made it a tempting target to try using React Native. If successful, using React Native would mean that we could share code between platforms, greatly reducing both the cost of copying existing features and (more importantly) the cost of every new feature we’d need to build going forward.
At the same time, we wanted to be humble about the reasons why other companies have struggled to adopt cross-platform technologies. A common issue that we avoided is the need to work with existing native product code, a requirement that can significantly negate the productivity advantages of using a cross-platform framework.
There were three main questions that we set out to address from the beginning:
We decided to leverage React Native as a compatibility layer between the shared business logic from our existing React (Web) codebase and platform-specific APIs. React Native allows us to bridge the differences between the UI APIs on each platform: DOM on Web, UIKit on iOS, and Android’s View system. We chose to adopt this cross-platform approach because it lets us share business logic between platforms, leverage high quality open source libraries, and rapidly develop and QA new changes to the app.

One of the main benefits of our cross-platform strategy is that we are able to share business logic between web and mobile. In fact, all major screens in our app share the same business logic across platforms; it was easier to adapt the existing Typescript-based business logic to be platform agnostic than to rewrite the same business logic multiple times.
An area where we chose not to share code is in the UI layer above the business logic. The guiding principle that we used was to share as much code as practical (business logic and some views) while allowing for some level of divergence to allow for native-feeling UIs and to use the APIs and technologies that felt best for each platform.
On the web front, this means using CSS-based media queries to create a responsive website and using CSS for things like sticky headers. On mobile, we add support for pull to refresh and double tapping on the navigation bar to scroll to the top of a screen. For both platforms, the lowest level components (like buttons, forms, etc.) are implemented separately as part of our design system, which we talked about in a previous post.
By using React Native, we’re also able to tap into a rich open source ecosystem that has largely removed the need for us to write platform specific native code. Our experience has been that most common app use cases have mature, well-maintained libraries.
Some of our favorite libraries in use are:

Making a small code change and having it be reflected on device can take anywhere from a few seconds to several minutes in the largest apps. Reducing this incremental build time pays dividends beyond simple time savings. As the iteration time gets shorter, we’re able to both make more rapid and isolated code changes, greatly enhancing comprehension of the code that those changes are being applied to.
Both our web app (via Next.js) and our mobile app (via React Native) support Fast Refresh, allowing changes to components to be reflected in a matter of seconds without losing either app or individual component state. Fast Refresh has been indispensable. When I’m not sure of how a UI will look, I can save small changes several times in a row, tweaking one layout property until everything looks right. Frequently, I keep a browser window and simulator open side by side to reflect live changes on two platforms simultaneously.
As an early-stage company, we frequently ship new features and fixes to our app. In order to prevent new bugs from being introduced during this process, we made it easy for developers to manually test changes as a normal part of their review flow.
On web, we deploy every open pull request to a unique Google Cloud Run URL so that opening the new version of the app is a single click away.

On mobile, we generate a QR code that can be scanned by a phone running the Ambrook app to download and run the latest Javascript bundle live on the phone. Native code changes can’t be previewed using this system, but it’s worked well for us because most of our changes do not involve native code. The technical approach used here is similar to that of Shopify’s React Native app, which they’ve described in this blog post.

For Ambrook, building a cross-platform web and mobile app from a single codebase has saved us significant time and effort. Other teams considering a similar approach can take a few points away from our experience:
Finally, as a quick catch-all, a few bonus recommendations for adopting our stack:
Thanks to our lack of legacy code, we’ve been able to pick the best of breed tools for our situation: React Native on Mobile and React/Next.js on Web. This technology is instrumental in our ability to rapidly and efficiently iterate on our mission of helping to make farmers more profitable and sustainable.
As an early-stage company, it’s imperative that we build and test ideas quickly.

Build an exact model of an inexact world.
