How to manage staging and production environments in React Native 
Tags: reactnative, mobile, app, fastlane
Published: 2019-02-19 19:24:56

---
title: How to manage staging and production environments in React Native 
published: true
description: Use environment variables in React Native - learn how, here.
tags: react-native, mobile, app, fastlane
cover_image: https://thepracticaldev.s3.amazonaws.com/i/cue4yomwp0is1a12m5ah.jpg
---



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](https://around25.com/blog/manage-staging-and-production-environments-for-react-native-app/)*.

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 <a href="https://github.com/luggit/react-native-config" target="_blank">react-native-config</a> 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:

```javascript
// 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.

<img src="https://around25.com/blog/content/images/2019/02/1.png" alt="show ios product scheme" style="max-width: 100%;">

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
```

<img src="https://around25.com/blog/content/images/2019/02/2.png" alt="xcode 2" style="max-width: 100%;">

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.

<img src="https://around25.com/blog/content/images/2019/02/3.png" alt="add pre build script" style="max-width: 100%;">

<img src="https://around25.com/blog/content/images/2019/02/4.png" alt="add pre build script part 2" style="max-width: 100%;">

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 <a href="https://developer.android.com/studio/build/build-variants?utm_source=android-studio#build-types" target="_blank">build types</a>. In `android/app/build.gradle`, under `buildTypes` we have the two default build types, **release** and **debug**.

```gradle
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:

```gradle
stagingrelease {
    initWith release
}
productionrelease {
    initWith release
}

```

Each of those is going to be used for making a *release-type* build, so make sure to <a href="https://facebook.github.io/react-native/docs/signed-apk-android" target="_blank">generate a signing key</a> 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 <a href="https://chase-seibert.github.io/blog/2016/12/09/react-native-android-staging-variant.html" target="_blank">here</a>. 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 <a href="https://github.com/luggit/react-native-config/issues/175#issuecomment-343004822" target="_blank">here</a>.

Now, at the very top of the same `android/app/build.gradle` file, add this:

```gradle
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 <a href="https://github.com/luggit/react-native-config#problems-with-proguard" target="_blank">here</a>.

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:

<img src="https://around25.com/blog/content/images/2019/02/5.png" alt="android build variants window" style="max-width: 100%;">

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 <a href="https://docs.fastlane.tools/#getting-started" target="_blank">install fastlane</a> 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.

```ruby
// 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:

```ruby
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:

```ruby
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:

```ruby
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:

```ruby
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 <a href="https://github.com/calintamas/react-native-envs-poc" target="_blank">Github</a>.

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

Thanks,