Building Multiple Apps from One React Native Project
Many reasons can be thrown around for why you need a new variant of your product:
• Certain features are considered crucial in some markets and not in others
• The original app name and branding has less meaning in a new market
• A need to distance from the original business for a fresh start
• Market-specific regulatory concerns leading to removal or addition of certain features
The key takeaway from these points are that 2 things are going to need to be variable:
Now you could say “Well that encompasses everything, doesn’t that just mean we’re building another app?”. The answer to that really comes down to where you draw the line, but we’ll get more into that later.
So the goal is to alter how our app looks, and some of how it behaves. Ok, great, let’s just fork all the code to a new app repo and then start firing in the new stuff, right?
…No, probably not a good idea.
One of the key principles when writing software is Don’t Repeat Yourself, aka DRY. The idea is you write code that performs a task and is tested to verify it performs that task, and then you reuse it whenever you want to perform that task. I probably don’t need nor want to write 10 functions to draw 10 differently sized circles if I can write one function that allows me to draw any size. That’s going to be both more flexible and will slow the growth of the codebase, keeping it easier to maintain and I only need to unit test one function.
If you fork, then you’ve essentially copied and pasted all the existing code and are now maintaining 2 versions of it, which requires possibly duplicating work. If you’re building multiple apps with a lot of shared features, you’re going to want to strive for maximum viable code reuse. This will allow developer time to be used most effectively on solving new problems. The reuse will be at its simplest if it remains the same project. The big challenge with this is both allowing for the differences while keeping the shared code in a clean, maintainable way.
Picture that we want to build multiple variants of a t-shirt customization app, one called Shirttastic and the other called Shirtotron, and maybe another in the future called Shirtcrazy, each of which we want to brand and distribute separately.
We could just start littering control flow around our app that looks like this:
This is not a good idea.
If we do this, we’re going to make developers go digging around to find out where in the codebase the app-specific differences lie. We’re writing code that is also presuming different feature behaviour (Shirttastic thing is not Shirtotron thing?), which is also getting more into the territory where we may consider these to be distinct products. Let’s move on to better ideas.
Leveraging Native Abstractions
Although this article is written from a RN development context, it’s currently inevitable with this issue that you’ll have to do some native configuration directly. In fact, this may always be the case as it seems that RN may never consider this in scope, more on that later. Thankfully there are 2 abstractions that can help you out here.
• Xcode Targets
• Android Product Flavors
Both of these allow build-specific customizations within the scope of one project In an RN app, the main purpose this serves is to customize the native metadata and asset sets. So, let’s get down to how both of these work.
“A target specifies a product to build and contains the instructions for building the product from a set of files in a project or workspace. A target defines a single product; it organizes the inputs into the build system—the source files and instructions for processing those source files—required to build that product. Projects can contain one or more targets, each of which produces one product.” - Apple Developer Documentation
An Xcode target allows you to customize the app store / system display name, bundle identifier (application ID), launch and icon image sets. Actually, it allows you to customize a lot more, down to the linked libraries and detailed build rules, but for our purposes those customizations are key. Below illustrates these key options in the Xcode UI.
Now, to actually leverage your use of targets, you’ll also need to make use of what’s called a Scheme. To quote the documentation again:
“An Xcode scheme defines a collection of targets to build, a configuration to use when building, and a collection of tests to execute.”
So, it’s the scheme that actually decides which target(s) we’ll be building. Also, setting the configuration means we have the possibility to build debug, release, etc. schemes for both Shirttastic and Shirtotron, while still only having one target for each. Below is the location in the product menu, and window for creating a new scheme. Note that you select a target it’s associated with on creation.
We’ll return to how make use of this when discussing build scripts later.
Android Product Flavours
“Product flavors represent different versions of your app that you may release to users, such as free and paid versions of your app. You can customize product flavors to use different code and resources, while sharing and reusing the parts that are common to all versions of your app.“ Android Studio User Guide
Below is an example of how to define Product Flavours in your app build.gradle.
With the above productFlavors defined, we can now publish the app with 2 separate application ids on the play store. So that takes care of the distribution problem. But do we get anything else out of this small bit of config? Yes - the key for the product flavour corresponds to an asset directory name under your android/app/src directory, as seen in the next image.
What’s great about this approach is that it allows you to have your shared resources in the main directory. Then, you can add additional resources, or overwrite resources, within the flavour-specific directory. On build, the Android build tools will do a look up to the flavor directory first for the asset you’re referencing, then look in the main directory. So, we can include all the Shirtotron brand-specific drawable images within its res directory and the same for Shirttastic, then whatever they both use goes in main. This system is extendable for as many flavours as we want. As a bonus because this is a build-time selection, the unused assets will not be included in our distributable, keeping the file size down.
React-Native Level Customization
App Config & Feature Flags
Earlier on I cited littering app conditions around the codebase was a bad idea because developers would have to go looking for them. Well, the alternative to this is to centralize your variant specific differences in one location, a config file for that app. There are different ways you could structure this, but the example below uses setting environment variables as this will have practical benefits for us.
The above variables are defined in 2 files - .env.shirtotron.development and .env.shirttastic.development. In this example we are combining in-app display name and feature flags, you could choose to separate them if you wish. The development suffix allows us to have separate files for production and dev builds and thus also set some other differences.
For the app name, the use is quite simple:
When we want to display the app name, we’re simply inserting this value. That means no control flow, whatever is in the config is what we show onscreen.
Now let’s say we have an image upload feature. It has been decided it can’t go into Shirtotron because of copyright fears in the market that variant is for. So, we have disabled it in the env file. Let’s look at how to utilize that in an RN component.
With the feature flag approach, every app will have the same simple condition. Does this variant have image upload as a feature? If it does, render the component.
App-Specific Build Scripts
If you’re working on a RN app, there’s a good chance you have some iOS and Android build scripts defined in your package.json file, as is a common practice.
For building apps, a simple solution is to extend those build scripts to also include app-specific builds. It may look something like this.
We are able to use a combination of utilizing the environment variables and passing command line arguments to the react native cli. Now simply from running this build script, we will be able to build the version of the app we want with all the correct assets, text and features that we have specified via our use of feature flags and native project config.
And there we have it! For the low divergence case, we have pretty much everything we need!
Okay, so this approach above is not actually perfect, at the time of writing you are possibly going to have a few gotchas:
• React-native link only links the first target on Xcode. You’ll have to manually link libraries for further targets.
• React-native run-android may fail to launch the app if the app id differs from the package id
I opened issues for both of these on the React Native GitHub repo. The iOS issue is actually considered possibly out of scope for React Native as it apparently represents an ‘expert’ native setup that is beyond the majority use-case. It looks like the Android issue has an answer that I have not yet tested, but I’m planning to investigate.
Anyway, a general point here is that building for multiple apps was far from the React Native team’s top priority. The project is designed for the majority use case, and the majority use case is simply one application. That’s why you have to dig into native configuration and library use rather than simply using out of the box RN tooling.
Another limitation is that this approach will begin to break down if you stray from the intended use case.
Multiple Variants vs. Multiple Projects
The approach as I have described it is suitable for a specific case - when you are building different “flavors” of the same application. This is key - that at its core this continues to make sense as one project. These variants are the same thing, with tweaks.
This matters because we are making an important decision -- we are not going to allow significant divergence between these variants. If we have substantial divergence but we attempt to keep one project, then our system is going to get more complex than what we’re describing in this article. Doing more things with the same codebase often means more complex control flow.
I think a key indication of whether you’ve diverged too heavily comes from your feature-set between variants. If it’s simple a case of feature on/off and only a few cases of this, then that’s probably still the same app. If you get to a point where your requirement is to have features behave drastically differently between variants, or if there’s little to no shared features between the apps, then you can probably now say this is not the same application.
At that point a good way to enable code reuse is no longer having one project, but having multiple projects depend on one or more shared projects. For example, if Shirtastic and Shirtotron now have totally different features, but still require the same components to build them out, they could both depend on a separate project called ShirtComponents. You may even later decide it’s able to be generic enough to rebrand, open source and give to the community.
This shared component library idea is a good idea in general. Where possible, if you can envision the possibility of your components being reusable in more than one app, it’s good to be prepared for that by writing them decoupled from the application. You’ll probably write better, more testable code if it’s decoupled as well. So, if you were unsure about the likelihood of your app flavours diverging and wanted to account for the possibility, I would recommend splitting out your reusable work sooner rather than later, as it’s probably going to be a value-add either way and gives you flexibility.
The approach described in this article is what I personally chose to do recently for a project that had a requirement to build multiple apps with the same features, but different branding and some minor tweaks. This has worked well for us because of an intent not to diverge. As I said above, this may not be the right decision if you know that you’re going to encounter radical changes. If you’re unsure, use this article’s approach, but also begin taking other measures to prepare for a multi-project split.
Thanks for reading. Stay tuned for more React Native content in the future.