DEV Community

Cover image for How to manage staging and production environments in React Native
Calin Tamas
Calin Tamas

Posted on • Updated on

How to manage staging and production environments in React Native

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.

I originally posted this on around25's blog.

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.

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 react-native-config is 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 .env.staging and .env.production.

// .env
IS_PRODUCTION=false
API_HOST=https://api.staging.foobar.com

// .env.staging
IS_PRODUCTION=false
API_HOST=https://api.staging.foobar.com

// .env.production
IS_PRODUCTION=true
API_HOST=https://api.foobar.com

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.staging.foobar.com and 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:

// src/config/index.js

import env from 'react-native-config'

const config = {
  api: {
    host: env.API_HOST,
    timeout: 20000
  }
};

const API_HOST = config.api.host;

export {
  API_HOST
}

export default config

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:

$ ENVFILE=.env.staging react-native run-ios  

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.

iOS setup

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:

Product -> Scheme -> FooBar

where FooBar is the name of my app.

show ios product scheme

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:

Product -> Scheme -> Edit Scheme -> Duplicate Scheme

xcode 2

I'm going to call them FooBar.staging and 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.

add pre build script

add pre build script part 2

I'm going through exactly the same process for FooBar.production.

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.

Android setup

For Android, we have build types. In android/app/build.gradle, under buildTypes we have the two default build types, release and debug.

buildTypes {
    release {
        minifyEnabled enableProguardInReleaseBuilds
        proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
        signingConfig signingConfigs.release
    }
    debug {
        debuggable true
    }
}

Same as before, the default is going to use the local env vars.

For the new build types, add these lines:

stagingrelease {
    initWith release
}
productionrelease {
    initWith release
}

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:

project.ext.envConfigFiles = [
        debug: ".env",
        release: ".env",
        stagingrelease: ".env.staging",
        productionrelease: ".env.production"
]

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:

android build variants window

That's it for Android too. Next, we're going to automate our build process even more, by creating fastlane scripts.

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.

In 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:

$ fastlane ios beta --env=production

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.

// fastlane/Fastfile

platform :ios do
  desc "Submit a new build to TestFlight"
  lane :beta do
    app_identifier = "com.app.identifier"

    api_environment = "staging"
    if ENV["IS_PRODUCTION"] == "true"
      api_environment = "production"
    end

    ENV["ENVFILE"]=".env.#{api_environment}"

    puts "API_HOST: #{ENV['API_HOST']}"
    puts "IS_PRODUCTION: #{ENV['IS_PRODUCTION']}"
    puts "ENVFILE: #{ENV['ENVFILE']}"
    end
  end

If everything is right, we should see this printed out:

[18:16:52]: Loading from './/../.env.production'
[18:16:52]: Driving the lane 'ios beta' 🚀
[18:16:52]: API_HOST: https://api.foobar.com
[18:16:52]: IS_PRODUCTION: true
[18:16:52]: ENVFILE: .env.production

Before making a new build, we may want to increment the build number and/or build version. Add these lines to the file:

increment_version_number(
    xcodeproj: './ios/FooBar.xcodeproj',
    bump_type: "patch",
    # bump_type: "minor",
    # bump_type: "major",
    # version_number: "1.0.0"
)
increment_build_number(
  xcodeproj: './ios/FooBar.xcodeproj',
  # build_number: '74'
)

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:

gym(
    xcodeproj: './ios/FooBar.xcodeproj',
    scheme: "FooBar.#{api_environment}"
)

Finally, I'd like my script to also upload the build to TestFlight, so I do this:

pilot(
    app_identifier: app_identifier,
    email: "itunesconnect_email",
    first_name: "itunesconnect_first_name",
    last_name: "itunesconnect_last_name",
    ipa: "./FooBar.ipa",
    distribute_external: true,
    skip_submission: true,
    skip_waiting_for_build_processing: false
)

It's almost the same process for Android, but instead of the gym and pilot actions, we are using gradle and supply. The lane looks something like this:

platform :android do
  desc "Submit a new build to Google Play Console"

  lane :beta do
    app_identifier = "com.app.identifier"

    api_environment = "staging"
    if ENV["IS_PRODUCTION"] == "true"
      api_environment = "production"
    end
    ENV["ENVFILE"]=".env.#{api_environment}"

    puts "API_HOST: #{ENV['API_HOST']}"
    puts "IS_PRODUCTION: #{ENV['IS_PRODUCTION']}"
    puts "ENVFILE: #{ENV['ENVFILE']}"

    gradle_file = "./android/app/build.gradle"

    android_set_version_name(
      version_name: "1.0.0",
      gradle_file: gradle_file
    )

    android_set_version_code(
      gradle_file: gradle_file
    )

    gradle(
      project_dir: './android',
      task: 'assemble',
      build_type: 'release'
    )

    supply(
      json_key: 'google_play_console_key',
      track: 'beta',
      apk: './android/app/build/outputs/apk/release/app-release.apk',
      package_name: app_identifier
    )
  end
end

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.staging and .env.production with dummy/non-sensitive values that are replaced on the build machine only at build time.

So the three files would look something like this in the repo:

// .env
IS_PRODUCTION=false
API_HOST=https://api.staging.foobar.com

// .env.staging
IS_PRODUCTION=false
API_HOST=api_host

// .env.production
IS_PRODUCTION=true
API_HOST=api_host

The complete source code can be found on Github.

Feel free to tell me what you think or if you have any suggestions for the solutions I advanced above.

Thanks,

Discussion (28)

Collapse
bobrundle profile image
Bob Rundle

Had to add matching fallbacks to my custom releases to get this working on Android.

    stagingrelease {
        initWith release
        matchingFallbacks = ['release', 'debug']
    }
    productionrelease {
        initWith release
        matchingFallbacks = ['release', 'debug']
    }
Collapse
bobrundle profile image
Bob Rundle • Edited

Spent quite some getting env files to switch with build type. Discovered that you need to apply dotenv.gradle after envConfigFiles are defined.

project.ext.envConfigFiles = [
    debug: ".env",
    release: ".env",
    stagingrelease: ".env.staging",
    productionrelease: ".env.production"
]
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
Collapse
luizfbrisighello profile image
Luiz Felipe Shimizu Brisighello

In Android Studio there is no Build Variants for me at:
View -> Tool Windows

For some reason im not able to call a env variable as a path in like:
require(env.IMAGE_SOURCE_PATH)

Any ideas?

Collapse
waqaramjad profile image
Waqar Amjad

i have same issue

Collapse
calintamas profile image
Calin Tamas Author

Might be something related to react-native-config linking. Try reinstalling the lib

Collapse
jake41 profile image
jake41

ENVFILE=.env.staging react-native run-ios

This step is giving me error:

/node_modules/react-native-config/ios/ReactNativeConfig/ReactNativeConfig.m:2:9: fatal error: 'GeneratedDotEnv.m' file not found

import "GeneratedDotEnv.m" // written during build by BuildDotenvConfig.ruby

Collapse
smakosh profile image
smakosh

I have solved that here: github.com/luggit/react-native-con...

Collapse
calintamas profile image
Calin Tamas Author

It looks like an issue with react-native-config linking.
Check here: github.com/luggit/react-native-con...

Collapse
mahesh__nandam profile image
Mahesh Nandam

I succeed to solve this by removing react-native-config from Pod file and manually linking.

Collapse
teeccblog profile image
Teecc blog

So schemes in xcode are working for you?? i've always got errors building in custom schemes in xcode ever since i started working with RN. Actually i used this library in a time, that was suposed to solve the issue i mention github.com/thekevinbrown/react-nat...
Unfortunately it is not working for me any more -_-

Collapse
calintamas profile image
Calin Tamas Author

What exactly isn't working on your side?

Collapse
teeccblog profile image
Teecc blog

I am having the same error i've ever had: when i try to build in my custom scheme config, this error arises: apple mach-o linker error. When i switch back to release/debug it works again, so the problem relies on my custom schemes. I've seen some posts where people suggest to change some of the build variable folders, so the custom scheme seeks for resources in the release-build folder instead of the custom one. But it didn't work for me either.. I was just wondering if you had to perform an extra-step/configuration to make custom schemes work in your xcode-RN project.

Thanks for the reply,
Cheers

Thread Thread
calintamas profile image
Calin Tamas Author

Hi, not really. Just make sure to have the correct target for build on the new scheme.

Also, it's easier to just duplicate the default scheme when creating a new one.

Collapse
amandine16 profile image
Amande

I couldn't get the environment variables on android. There was a step missing.
In the file android/app/build.gradle, you have to add a line that is not indicated in the tutorial.

The tutorial is very complete.

This line was missing: apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle "

I had to insert it after project.ext.envConfigFiles and my other plugin apply ... Otherwise it didn't work.

Hopefully I helped someone =)

Here are the first lines of code in android/app/build.gradle

project.ext.envConfigFiles = [
debug: ".env",
release: ".env",
stagingrelease: ".env.staging",
productionrelease: ".env.production"
]
apply plugin: "com.android.application"
apply plugin: 'com.google.gms.google-services'
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
import com.android.build.OutputFile
...

Collapse
jadhavrahul10 profile image
rahul

Hi,
Actually i want to setup 3 env. i.e dev, stage (Flavor) ,prod( release ) and
1.for dev it's debuggable by default
2.for stage i want to setup debug.keysore (for debugging ) and release.keysore (for release testing we can se pre prod ) different
3.And prod release i want different prod.keystore how can i manage this thing.

hoe can i mange this things in buildTypes , productFlavors , signingConfigs?

Collapse
jotahws profile image
João Henrique Wind

Just a heads up for those trying to do this in a newer version of react-native-config.
According to the docs, the new Script Action for the iOS scheme should be the following:

cp "${PROJECT_DIR}/../.env.staging" "${PROJECT_DIR}/../.env"

and NOT

echo ".env.staging" > /tmp/envfile

Reference:
github.com/luggit/react-native-con...

Collapse
jaapweijland profile image
Weijland

Note that since v 2.148.0 of Fastlane the follow line:

fastlane ios beta --env=production

should be

fastlane ios beta --env production

Collapse
suleiman365 profile image
Suleiman Ahmed

Great tutorial, Calin.

Small issue, please.

"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:"

The above step doesn't seem to work for me. Perhaps, I'm doing something wrong on the Android front. When I do the above, I noticed that the newly added release configs (i.e productionrelease or stagingrelease) doesn't show in the dropdown of the other third party native packages, such as 'react-native-config', for example. And when I go ahead to select, say, 'stagingrelease', I get this error: ERROR: Unable to resolve dependency for ':app@stagingrelease/compileClasspath': Could not resolve project :react-native-gesture-handler and this: ERROR: Unable to resolve dependency for ':app@stagingreleaseUnitTest/compileClasspath': Could not resolve project :@react-native-community_async-storage.
And so on (listing all my third party packages).

Thanks.

Collapse
bobrundle profile image
Bob Rundle

This is the same problem I had. You need to have fallback build type because you have other projects for which staging and production releases are not defined. See my post in these comments.

Collapse
jadhavrahul10 profile image
rahul

How to manage firebase ( google-service.json and google-service-info.plist ) for android & ios for load configuration as per there env file parameter

Collapse
calintamas profile image
Calin Tamas Author

Hi, rahul

on Android you can have both config files stored somewhere in your app source

config/firebase/production/google-services.json
config/firebase/staging/google-services.json

And write a bash script that copies these files in the android folder at build time (run the script with fastlane):

#!/usr/bin/env bash

if [ "$IS_PRODUCTION" == "true" ];
then
  echo "Setting Firebase production environment"
  yes | cp -rf "../js/config/firebase/production/google-services.json" "../android/app"
else
  echo "Setting Firebase staging environment"
  yes | cp -rf "../js/config/firebase/staging/google-services.json" "../android/app"
fi

and on iOS, add both files to the Xcode project

GoogleService-Info-production.plist
GoogleService-Info-staging.plist

In AppDelegate.m, select the correct file for your env

NSString *isProduction = [ReactNativeConfig envFor:@"IS_PRODUCTION"];
NSString *firebaseConfig = [[NSBundle mainBundle] pathForResource:@"GoogleService-Info-staging" ofType:@"plist"];
  if ([isProduction isEqualToString:@"true"]) {
    firebaseConfig = [[NSBundle mainBundle] pathForResource:@"GoogleService-Info-production" ofType:@"plist"];
  }
Collapse
jadhavrahul10 profile image
rahul

How could i generate build android and for IOS as per env param?

Collapse
giacomocerquone profile image
Giacomo Cerquone

Hi, let me know what do you think about my alternative solution: dev.to/giacomocerquone/re-thinking...

Collapse
calintamas profile image
Calin Tamas Author

Nice way of doing it!

My reason for using react-native-config was that I needed to access the env vars inside the native projects as well.

Collapse
giacomocerquone profile image
Giacomo Cerquone

Well in that case yes, it's a must :)

Collapse
sauloaguiar profile image
Saulo Aguiar

Hey Calin!
Thanks for coming up with this tutorial!
After I replicated the steps, I wasn't getting any error but my .env.staging file wasn't being read.
I noticed in your github repo that for the android project there's an apply rule for the react-native-config project in the buidl.grade file.

apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"

Collapse
praveens96 profile image
praveens96

need to do add Library Search paths in-order get it working, for me I have Realm, YouTube etc libraries for which .a files were in Linked libraries. I was getting ld: library not found -lRealmJS error.

so, I followed: stackoverflow.com/a/40661027/4065202 to resolve the issue

Collapse
wkwyatt profile image
William

Do you have an example of the script you used to run the env variable setup for the iOS schemes? I check the GitHub project and it wasn't there.