Your life as a React Native developer is just about to get easier. At least mine did, as soon as I learned how to manage staging and production environments in React Native. It doesn't change the way your React Native app looks, feels or sells. But it saves you a great deal of developer hustle.
It's a breeze to change a config when there's only one or two changes to be done. You switch the
API host to the production one and bam, you're done. Things only get harder and much more error-prone when you have to change the
API host, put the payment gateway
key, payment gateway
host, set the notifications
.plist file, update the crashlytics
key and update the analytics
key. Do this by hand every time you make a build and at some point I guarantee you're going to miss something.
So to avoid headaches, we're going to use environment variables.
If you're a React Native newbie, check this comprehensive React Native guide from Netguru.
To expose env variables to
react-native, I'm going to use the react-native-config library. By doing so, I am keeping the config variables away from the code. It's most likely that those vars are going to be different across environments, while the code is not.
First of all, I'm going to add the library to the project. As I'm writing this, the latest version for
0.11.7. Then, I have to create 3 env files in the
root of the project: one for the local environment, simply called
.env, and another two called
For now, I'm only keeping the
API host in the config, alongside with an
IS_PRODUCTION flag. We'll use that later. Obviously, both
https://api.foobar.com are fictional 🙂.
Now, to put those vars to use for my React Native app, here's how my config file usually looks like:
That's all. By default,
react-native-config reads from the
.env file, but reading from any file & running the project is possible via the following command:
All good so far, but I want my build process to be as automated as possible. So manually specifying the
.env file is not acceptable. Avoiding this is what we're going to do next.
On iOS, I'm going to take advantage of the concept of schemes. An Xcode scheme defines a collection of targets to build & a configuration to use when building. The default one can be found in Xcode's menu bar:where *FooBar* is the name of my app.
What I'm going to do is duplicate that default scheme and create two new schemes: one for staging and one for production. We're going to use the default one for the local environment. You can do it by going to:
I'm going to call them
FooBar.production. Naming is going to be important when we write the automated build script later.
I'm going to select
FooBar.staging and go back to
Edit Scheme, to make sure my scheme loads up the
.env file I want. Here, on
Build -> Pre-actions I'm adding a script that does that.
I'm going through exactly the same process for
Now, I got everything set up for iOS. Whenever I'm building the app, by selecting a scheme, I get the right env vars loaded in. Kudos, let's go to Android.
For Android, we have build types. In
buildTypes we have the two default build types, release and debug.
Same as before, the default is going to use the local env vars.
For the new build types, add these lines:
Each of those is going to be used for making a release-type build, so make sure to generate a signing key before proceeding.
Naming is very important at this step. First, you must have the token release in the build variant name for regular release build behavior to apply, see more here. Second, I had issues when I initially set up the names to releaseStaging and releaseProduction, as the correct
.env file was not loading. I googled it up and found the issue & solution here.
Now, at the very top of the same
android/app/build.gradle file, add this:
You might encounter problems with Proguard for release builds. The solution is here.
To run the app using one of the build types defined above, go to
View -> Tool Windows -> Build Variants and select the variant (in the newly exposed window) before building:
That's it for Android too. Next, we're going to automate our build process even more, by creating
My aim is to have a one-liner build script for each of the two platforms.
Bonus: fastlane scripts
First, we are going to install fastlane and go through the setup.
fastlane init has to be run inside an iOS or Android project, so we're going to do it inside the ios folder and then move the whole
fastlane folder to the root of the project.
fastlane/Fastfile, I'm creating a lane under the ios platform to upload a build to TestFlight. Fastlane automatically loads the .env file we are passing when running the script. If we do:
it will load the
.env.production file. So we are all set. To test this, I am going to add a few lines that only print out the environment vars and the file that was loaded.
If everything is right, we should see this printed out:
Before making a new build, we may want to increment the build number and/or build version. Add these lines to the file:
Now, to create a build, add the following lines. Remember that at the beginning of the article I mentioned that the naming of the schemes will be important later. Well, this is why:
Finally, I'd like my script to also upload the build to
TestFlight, so I do this:
It's almost the same process for Android, but instead of the
pilot actions, we are using
gradle. The lane looks something like this:
Final thoughts on Staging and Production Environments in a React Native App
Before I leave you, I'd like to express my thoughts on what I believe it would be best practice regarding source control.
I suggest committing real values to
.env (the local environment), but keeping
.env.production with dummy values that are replaced on the build machine only at build time.
So the three files would look something like this in the repo:
The complete source code can be found on Github .
Feel free to drop me a line here to tell me what you think, discuss anything React Native-related or if you have any suggestions for the solutions I advanced above.