I had to check Medium, because I felt like perhaps I wrote a draft of this story 15 months ago. That was the last time I thought “perhaps we should rebuild our app in React Native.” That lasted a few weeks as I made the shell of an app, discovered various roadblocks and got a sense for how hard the overall task would be. It did not reach activation energy then. There were many reasons for that, both from a maturity standpoint in React Native and the circumstances at GasBuddy. We still retained the majority of the team that built our native apps from the ground up more than a decade ago. Most of our iteration was backend heavy — we built a payment product that sold a half a billion dollars in fuel and we integrated telematics and vehicle recall notifications and such. Having to build the front end to those features twice (iOS+Android) and sometimes thrice (iOS+Android+Web) was annoying, but not prohibitive.
On the React Native side, there were several big blockers that made me less than excited about the long road to feature parity with the existing apps.
- Navigation was messy. react-native-navigation seemed the most mature option, but it was incredibly finicky to get working, and as new version of React Native came out, even in the short months I spent dabbling, I had to do those finicky things all over again.
- Image handling was daunting — SVG is clearly the right answer for source material, but it seemed to be poorly integrated into the workflow of RN apps. That may have just been my lack of understanding, but it was a concern for large scale development.
- Native modules were a complete pain. Anyone working with RN before automatic linking would likely agree that it was messy — modifying pod files, native code on both platforms, managing version clashes — it was just drudge work. 99% of that pain is just plain gone.
Additionally, it was about 6 months after the infamous AirBnB middle finger to React Native. I think the articles were quite balanced and honest, but every time someone wants to bash RN, these articles are point number 1. I feel that misses some key nuances. First, a great many of the problems AirBnB had have in fact been addressed. They were a victim of timing, and their loss and pain are the rest of the ecosystem’s gain. Second, GasBuddy is not AirBnB and if you’re at a technology company considering React Native, you probably aren’t either. Their native app teams are probably bigger than our entire company. Their app is known for leading edge design, animation and visual polish that is hard to achieve with anything between you and the hardware.
So, fast forward 15 months and a variety of changes made me wonder whether it was time to take another look. I started out the same way as before, but with the learnings of the past I had some demands of the code:
- Death to Redux. I do not like Redux. I do not like repeating myself. I do not like strings as identifiers in code. I do not like four files for every single thing the app needs to do. In the React world, I prefer unstated. I initially expected to use unstated-next but eventually decided on mobx-state-tree even though it’s a little heavy handed.
- Typescript. This wasn’t a must have at the beginning, but people I respect had recently started a React Native project and used Typescript, so I figured we’d give it a try. In retrospect, it’s been instrumental in making a pleasant and fast developer experience, mostly because of its impact on Intellisense in Visual Studio Code. Getting some veil of type safety for free was a nice addition. This is another place where time has helped a great deal. The number of RN-related modules that have Typescript support now is significantly higher than what I recall from the last attempt.
- react-navigation. It has matured a great deal over the year, performance appears on par with native navigation on modern devices, and there is no fuss. The declarative model seems a great fit for modern React and works smoothly with hooks and contexts and Storybook and all such things.
Perhaps as importantly, our circumstances have changed. We are focusing more on front end iterations like “search along route” and in-app personalized gas discounts and now spend a high percentage of our time building the same things twice. We are also just experimenting more often, trying to find the perfect copy or the best way to onboard casual app downloaders into GasBuddy members and users of our Pay with GasBuddy product. We’ve built a bunch of server side infrastructure to enable that experimentation, but honestly a bunch of it is just bending to the realities of upgrading mobile applications. Some of those original developers have also moved on to other opportunities, and as it was several of their first jobs in industry, I can’t really fault them for that.
Of course all code eventually needs to die. We’ve made lots of changes — to Swift and Kotlin, to a new service layer, etc — but there is no denying that there are plenty of cobwebs in our codebase. Dark corners that everyone is afraid to touch or has no clue what they even do anymore. By the way, that turns out to be one of the biggest determinants of migration speed in React Native: our ability to find documentation or even descriptions of why certain things do what they do. How is login supposed to work when you use a Google sign in to an account for which you no longer have access to the email address? How does the current search persist as your location changes if you had been on some sort of search along a route? The list is endless and each of them takes nontrivial time to reverse engineer from the code. Often the two platforms don’t even take the same approach, and I suppose that’s no surprise since the code bases are entirely separate.
As we began this second attempt at React Native, I felt it was important to have some experts available to guide us, and got a recommendation from a friend for Infinite Red. In addition to just extra horsepower that didn’t detract from the existing roadmap, they helped us avoid a great many pitfalls, while still allowing us to have our own opinions and explore some new ground for both of us. React Native has been a fast moving platform since inception, and experience is extremely valuable and best when loosely held, and they’ve done a very good job of balancing that in our case. We have made great use of a shared Slack channel, and I would argue that channel has been the most “irreplaceable” part of our engagement with Infinite Red. They’ve done a tremendous amount of “real work,” but in terms of figuring out whether React Native is a realistic option for GasBuddy, the thousands of random questions and explorations we’ve done in that channel have been much more valuable. I think it’s fair to say there are innumerable blind alleys left in a production high-visibility React Native development effort, and it is crucial do avoid those which have deep pits at the end.
I made some big process decisions early that shaped our work in the first month.
- We were going to build from the ground up. We were not going to integrate some React Native screens into the existing app, even as a proof of concept. I believe many of the gains React Native may deliver will require not carrying the baggage of the existing apps.
- We were going to replicate the existing app design as closely as possible. There are plenty of reasons to redesign our app, and you can argue that combining a redesign and a re-platform would be more efficient than doing them independently. Since we don’t have a completed redesign, I felt this just meant we would never finish the re-platforming. Our goals are around efficiency and flexibility, so the longer we delay sunsetting the existing apps, the longer we end up having to build each feature 3 times. Now, as a practical matter, if we know redesigns are coming to elements in the app long before we would be done with React Native, we will wait for those to be completed in the existing app before porting them to React Native. If and when we truly decide to go to React Native, I believe there will be redesigns that we choose to execute first on an unreleased React Native app, because we can user test more easily, perhaps even beta test more easily and flesh out all the non-mobile elements (supporting back end services, etc) ONCE on React Native and then just copy the design to the native apps without having to wait for any supporting changes.
Given those decisions, the Infinite Red developers and I set about building as much of the app as we could in a month. We made it pretty far — navigation, registration and login (simplified), service interfaces, state management, basic location management, gas station listings… All of these worked well enough to fool you into thinking they were done. They looked close to pixel equivalent to the native apps. They performed well enough. The code was maintainable, clear, and predictable. We had some basic tests, CI/CD, style guides, setup scripts… It was a real project. We decided it was time to share it with the broader teams.
The #1 thing that would kill React Native at GasBuddy would be our engineering team deciding it wasn’t worth the effort, or wasn’t the right long term decision. We made a pretty good demo with some elbow grease and experienced help from Infinite Red. The work we did was not throw away work — the code is a solid and real foundation for a full blown app. It would be naive to say, however, that we were 25% done, or even 10% done. So the idea that we could get to 100% without meaningful involvement from the existing team, not to mention the horrible position that would leave us in at the end, is wrong. It will likely take us 5–6 months with 4–5 resources at the least to have a fully working app. So we need coverage on the existing apps in that time, and we will need to reduce our overall output to accommodate direct and indirect work on the React Native app. It is eminently reasonable to think this is too high a price to pay, and even more so if you aren’t convinced of the benefits or the likelihood of success.
So we did a big demo, described the merits of React Native, and etc. In general I think it’s fair to say people were impressed at the progress in a short time with a few people. But our team pointed out a very obvious thing that hadn’t really occurred to me until then — they don’t even have time to EVALUATE whether React Native is a good idea or not. They’ve got 70 points to finish every sprint, and this would take a bunch of them. I imagine the response at most companies of our size would be the same. We decided that what might be most helpful is if we could all focus on React Native for some short period, and “React Native Week” was born. It took some time to convince the rest of the company that a 1 week “work freeze” was worth it. We came close to cancelling the whole damn thing. But we decided it was now or never.
I plan to keep updating this article as we make a final decision. If we choose to move to React Native, I plan for us to do it “in the open” as much as possible so perhaps our experiences help someone else. While I don’t think we are likely to open source the GasBuddy app, whatever modules that we can isolate that might be generally useful we will open source from the start. We’ve got 3 so far, hopefully of some use to someone!
We needed a workflow that accommodated SVG and PNG inputs and produced easily usable components. In the case of SVG, it needed to convert them to Typescript-safe components and for PNGs I’d prefer to store one image in the repo and generate the other resolutions automatically in the common cases. I did not find tooling that let us do all these things, so we wrote some. react-native-image-builder takes a directory of image files and produces typescript files and scaled PNGs in a way that can be built and consumed as a module in a monorepo.
It’s been a long source of embarrassment that the GasBuddy app is only available in English. It is not purely a client side problem (not to mention marketing emails and the like), but it is predominantly one. We wanted a simple, typesafe mechanism for defining localized, templated strings. We decided on a yaml format and open sourced the react-native-strings module that converts templated string specifications into Typescript and then runtime values. We will also be extending this to allow runtime overrides of strings from the server, even though this is marginally useful once you have CodePush and dynamic updates of the whole bundle.
The navigation stack of an app can get fairly complicated. Nested stacks and modals, tab navigators, top tabs… oh my. Each of the screens in this web has a name, and some have typed parameters. I found myself repeating the same strings and types multiple times for more complex scenarios and I didn’t like it. So you are probably detecting a theme from the previous two examples, and wedid the same — created an intermediate format where we specify the structure of the stacks, the parameters each screen takes and the names of the screens (with sensible defaults). The react-navigation-codegen module is part of the build script for a low level module (called infra in our case) and allows us to navigate to screens with full type safety and not have to write lots of painful type declarations or worry about complex cross-module dependencies on where these types and screen name values are defined.
Gotchas (and solutions)
For our setup, storybook and the app are separate “small” modules in the monorepo. We want to be able to simultaneously run the storybook bundler and the main app bundler, and thus have to set them to run on different ports. This turned out to be tricky to do cleanly. Running the bundler is easy, we modify package.json in the storybook app to be:
"start": "react-native start --port 8082",
But it proved to be much harder to modify the iOS project to default to this port. We had to modify the post-install step for our storybook Podfile with this at the end of the main target section:
post_install do |installer|
installer.pods_project.targets.each do |target|
if target.name == "React-Core"
target.build_configurations.each do |config|
if config.name != 'Release'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)', 'RCT_METRO_PORT=8082']
That magic spell modifies the build config for the React-Core CocoaPod to set the default metro port. On Android, it was simpler once I knew where to put it. In gradle.properties in storybook/android, I added a single line: