In this article, we will look at how by adopting a new two-repo approach to React Native development we are able to improve team efficiency and also opens up doors to many other options that were previously left out of reach due to the unique triple-domains nature of React Native.
The new approach itself is relatively simple to implement as the majority of work is in moving the native /android
and /ios
folders three levels deeper into the project’s folder hierarchy, the result of this small change is the ability to separate the React(JS) and Native(Android, iOS, builds) domains in React Native by splitting a traditional single cross-concern RN repo into two, one repo for the native parts and the other for the JavaScript parts. And as we will see in this article, this change comes with a multitude of useful benefits for react-native projects of all sizes.
A while back Wix Engineering shared a glimpse of their React Native app architecture for increasing the development efficiency in their large teams of 50+ React Native developers. While their original article looks at development problems from a very large project point-of-view(the 1% of RN projects), after digging deeper into the internals it soon occurred to me that there are parts that can be utilized to benefit even the smaller development teams(that’s us - aka the 99%).
To validate my assumptions I’ve decided to test this new idea on the opposite end of what Wix did, by fully implementing it in a one-developer React Native project, and the end result is surprisingly positive.
Because the core changes of the two-repos approach are very close to the base levels in our software design decisions, it impacts many of the leaf decisions that came after it. To explain the new approach without information overloading everyone, I’ve decided to split it into two parts.
Here in Part One, we will look at the impact from a mostly high-level point-of-view so we can examine the various second and third order of consequences of applying this to React Native development.
You can think of Part One as the “What and Why”, whereas in the future Part Two we will discuss the “How to do X,Y,Z” where we will deep dive into all technical bits from feature developments to going live on the app store using the two-repos setup.
And for those who want an earlier hands-on experience before Part Two, at the end of this article, you will find a small demo repo for you to try out.
Currently, the most widely adopted React Native project structure is some variance from the default project setup you get when you first initialize an RN app. It’s basically a single repo containing all three domains of React Native: Android, iOS, and JavaScript.
Our new approach is a challenge to this current norm by splitting up the React and Native parts, and we will look at how this single decision is able to impact many other aspects of React Native software development.
First of all, we achieve native/JavaScript codebase splitting by moving the native folders three levels deeper. In the JavaScript-repo, instead of building the native binaries during development, we pull in the prebuilt binaries as dependencies.
For the developers, this process is exactly like adding any other libraries to a JavaScript project. I.E. npm i lodash
To achieve the main goal of determining the viability and practicality of this two-repos approach in real-life React Native development, I have set up the following test plan to validate the idea and used one of my live React Native applications BusDue as the testing ground.
Assumption :It’s viable and practical for greenfield app development
To simulate greenfield app development, I decided to completely rewrite BusDue and make some large changes along the way so a lot of things are newly written from scratch. The backend also went through a similar rewrite at the same time so we are closer to the rapidly changing environment of an early-stage greenfield project.
For example, the entire API was migrated from node/express to GraphQL+AWS Lambda. FrontEnd code went from JS to full TypeScript. State management also went through a redesign with more states delegated to hooks or the GraphQL client.
Some of the app functionality changes were made on the spot(I am the product/designer/developer :P) and sometimes reverted soon after because the end results weren’t what I wanted, this allowed me to test things in the early-stage setting where everything needs to be very flexible and react quickly to the constant requirement changes.
Assumption:It’s viable and practical for brownfield app development
Although the BusDue app’s business logic is largely a rewrite, there are still some parts and know-how that need to stay the same for backward compatibility reasons, for these, I need a port over and keep their existing behaviors so I don’t break current users of the app when they upgrade to the new version. For example, the reading and writing of data stored on a user’s device must be backward compatible.
Assumption:It’s viable and practical for small and mid size team
I am the only developer on BusDue, and since Wix already proved this works with 50+ developers, if I can prove this works with one developer, we have a very good chance that everything in the middle will also work.
After going through the whole process of rewriting and releasing BusDue v5 using the new two-repos setup, my conclusion is that this new development approach offers many benefits for both greenfield and brownfield projects.
And best of all rather than being a bunch of mutually exclusive decisions colliding against exciting practices, these benefits can be incrementally and optionally adopted, or further customized to your project needs.
More dev team composition options. The new two-repos approach make it much easier to incorporate JavaScript/React Web developers into your project.
Despite the name React Native seemingly calls for developers with skill in all three domains Android, iOS, JS, and a whole host of related knowledge such as app stores management and mobile CI/CD, when we actually look at the overall workload over a longer period of time we can see that they are not exactly linear.
For example, the native workload dominates at the beginning of a project and then slowly settles down over time, and there will be the occasional large spikes that require immediate attention for example to fix a blocking native bug or large RN upgrades forced by one of your dependencies.
For the majority of smaller projects out there having 2 or 3 RN developers with good native skillsets should be sufficient for most native work as the workload on the native side doesn’t really scale in relation to the feature/business development side(see above chart), it’s not uncommon to go through periods of little to no native changes.
You can certainly get away with just one native-focused developer at the start, but over the longer term, you increase the risk of developing bus-factor problems if you don’t duplicate this part of your team.
Support large feature development teams with just a few developers
With the native side of things covered, the rest of the dev team can be a mixture of RN or React/JavaScript developers with the main feature development which happens almost entirely on the JS side.
And for teams with access to existing React web developers and looking to onboard them into the mobile app project, this setup also offers a more granular approach compared to the learning curve one must take on in the single-repo setup, this results in a much faster path to productivity regardless of which area the new dev decides to focus on first.
Being able to only think in one single domain (native or JavaScript) is a great DX improvement
There is a substantial benefit in DX when working on the two-repos setup. This is because when working on the native side, you don’t have to worry about understanding or accidentally breaking any complex business logic since the Native-repo doesn't contain any code from the JavaScript-repo.
The same is true for devs working on JavaScript-repo tasks because the native binary used to run the app during development is imported as a node module dependency. You will always have the assurance that you are using the same well-tested native codebase as your colleagues, as well as not needing to fix any build problems that arise when you have to build the binary with your machine setup.
As mentioned in the previous section, when working on anything native related developers only need to think in the realm of native context and not have to worry about the project’s JavaScript code, and because of this clear separation, we are also free to write any kind of JavaScript code needed to test the native code is working.
We can even commit these test codes if needed and they will only show up when we run the app from the native repo. For example here is a comparison of a typical “add a native library dependency” task.
Much faster interaction cycle and no need to deal with JS codebase errors.
As we can see the developer working on this native task is able to iterate much faster due to their ability to boot up a much smaller RN app. And by not including the various complexities in our JavaScript codebase, we also save time by removing the need to read through UI code to figure out an appropriate spot to put the temporary test code. These small savings can really add up over time.
Instantly load JS into well-tested binaries, no need to deal with any build problems on your machine
The time and mental energy saving here is similar to the native example above but just the opposite, we eliminated the native binary build times between fresh app start of the application as well as gaining the assurance that the native binary you are working is identical to everyone else’s.
For larger projects, being a pure JavaScript repo means we now can make better use of other many other known and tested JS code-splitting techniques such as monorepo, or micro frontends development. In the old standard single repo setup, a lot of these development techniques are very difficult to implement efficiently due to the extra native elements in a React Native project.
Because we have a clear separation of JavaScript and native codebases, the commit history on each repo will also be more closely aligned to their actual evolution over time. This makes it a lot easier for our new React/JavaScript developers to make their first foray into the native side once they settle down, or vice-versa for more native-oriented developers looking to dive deeper into the world of JavaScript. This will also benefit all developers as it can drastically reduce the search area during debugging.
When considering whether to adopt a big decision such as this, not only do we need to evaluate if the benefit applies to our own individual situations, but we also need a good understanding of the various potential risks that we might encounter.
I think there is a very low chance for RN to remove support for custom file paths because the concept itself is nothing new, it is pretty much an essential functionality that enabled setups such as monorepo.
And AFAIK there are currently many React projects out there that are inside some sort of monorepo structure and each of them probably has its own folder hierarchy design.
As for other RN libraries, my BusDue app uses many popular native libraries such as react-native-maps, react-native-navigation, react-native-bugsnag etc.
I have yet to encounter any problems with them even though the node module they reside in is three levels up.
So based on the experience so far I think we can safely assume support will continue for the foreseeable future.
It’s a win here for the new setup.
While I can’t speak for the future but at the time of writing this article I’ve already gone through two react-native upgrades under this two-repo setup. The upgrade process is no different than your standard setup, in fact, I would say it’s easier to upgrade react-native in a two-repo setup because we have faster native debugging cycles due to the fact that we don’t need to load up a huge JS codebase each time.
Yes. As you can see in this example commit the whole change basically consist of two main parts, “move native folders down 3 levels” and “adding some QoL scripts and tooling to aid development”.
For the latter, it’s less mysterious as it sounds, all the scripts and tooling are just helper functions that ultimately produce a line of a standard Xcode or Gradle command-line script which we can run in a standard terminal.
For example, our yarn build-ios
script simply constructs a xcodebuild <args...>
command for building the and ios archive, and the yarn ios
script constructs a xcrun simctrl
command for launching the app in a simulator. All these are exactly the same commands React Native itself prints out on the console during a normal build or run process.
So if you ever want to revert this all you need to do is move the folders back to where they were and remove the ../../..
from various path settings then you will get back a standard React-Native project.
The deployment process is mostly technical steps so my plan is to defer that part to Part Two. But just to give you an idea of what it’s like, here are the general iOS steps for shipping an app store binary for release.
Native-repo
JavaScript-repo
npm
or yarn
react-native bundle
command.fastlane
(The process for Android is pretty much identical using Android equivalent commands)
Native-repo example build and publish your own native binaries
Companion JavaScript-repo that uses the binary from the native repo
Note that the above demo projects are a slim-down ios-only version. It’s not the final setup that I use in my BusDue app, for example in my BusDue app each time I build the native codebase I am able to output any number of these binaries for different purposes.
After spending time developing and shipping a whole app rewrite under this architecture and then comparing the old process I’ve been using in the past, I really like the simplicity of this idea and all the various developer-empowering benefits it brings, I will definitely continue to explore and refine this setup in my current and future projects.
Of all the listed benefits, my number one favorite has to be that I no longer need to think about half of the stack during debugging, and since 90% of development time is spent on various forms of debugging, this really freed up a lot of my time and mental energy for other important things.
I honestly believe this two-repos development process is a very strong alternative to any React Native projects starting today, or brownfield projects that have hit the scaling wall due to the various pain points we discussed.
I hope you find this article useful and will consider giving the two-repos set up a try in your next project, thanks for reading, and happy coding!
Also published here.