How to setup multiple environments in React Native

How to setup multiple environments in React Native

In this article I'll explain a bit more about how to setup different environments in React Native and how to properly manage them across Javascript and native code.

To fully understand it you need to have knowledge in Javascript, React Native and understand a bit about React Native's project native structure (iOS and Android). So… let's start.

Motivation

That article is like a puzzle I grouped all the important and valuable information that I’ve found during the implementation of environment setup in a project. Most of the articles or tutorials that I could find about the theme did not approach both, Javascript and Native code. So, I decided to do it.

I will be happy if it helps you in any aspect 😄

Understanding the problem

In a real project, we usually have three main environments (Development, Staging and Release). Those have each a different environment setup, like different API urls or Firebase projects.

But, how can we properly setup these variables according to the type of App that we want to run?

Since we usually have a lot of configurations that depend on which environment we are running, all of you agree that changing them by hand is not a good idea, right? In that case, we need to write some scripts to automate it, in both JS and Native sides. And that is exactly what we are going to see during this article.

Setting up Javascript Environment

Now that we know the problem, we can start to solve it. First, we need to setup different environments for the Javascript side of our project. That step is a bit easier and straight forward, since we just need to create some files and write a script to replace them before running the application.

First of all, we need to create the folders and files structure. In our project src path, create a config folder with another folder called env inside it. For each of the environment types that we have in our application we need to create a folder under the env directory. The final structure will be something like that:

src

└─ config

  └─ env

    └─ debug

    └─ staging

    └─ release

Each of the folders will have its own index.js file, like in the example bellow:

// config/index.js
export * from "./env";
 
// config/env/index.js
export { env } from "./debug";
 
// config/env/debug/index.js
export const env = {
  // Here you can put all your Debug environment variables accessible from Javascript.
  name: "DEBUG",
};
 
// config/env/staging/index.js
export const env = {
  // Here you can put all your Staging environment variables accessible from Javascript.
  name: "STAGING",
};
 
// config/env/release/index.js
export const env = {
  // Here you can put all your Release environment variables accessible from Javascript.
  name: "RELEASE",
};

Note: I like to work with that type of structure folder/index.js, but, feel free to modify it according to your project, and keep in mind that this will affect the code that you need to write in the following steps.

The environment structure of our project for the Javasript side is created. But we need a way to run our application using each of these configurations. To do so, we can write a script that replaces export { env } from './debug' inside the config/env file with the right environment export string.

To validate the params of our node script we are going to install yargs library:

yarn add -D yargs

Then, we are able to create the following script that will replace the line mentioned above and make it possible to choose what environment we want to run. The most important parts are commented with the explanation. Please, take a look:

#!/bin/node
const fs = require("fs");
const path = require("path");
const { argv } = require("yargs");
 
const { env } = argv;
 
// Accepted environment params, they are directly related to the name of the folders created before.
const acceptedEnvs = ["debug", "staging", "release"];
 
// Function that writes on the file.
function writeFile(file, string) {
  if (fs.existsSync(file)) {
    fs.writeFileSync(file, string);
    return true;
  }
 
  console.log(`File "${file}" not found.`);
  process.exit(1);
}
 
// Function that validate if the param passed to the script is a valid environment.
function validateParams() {
  console.log(`Validating params...`);
  if (!env) {
    console.log(
      `Error.  Please inform a valid environment: ${acceptedEnvs.join(", ")}.`,
    );
    process.exit(1);
  }
 
  if (!acceptedEnvs.includes(env)) {
    console.log(
      `Error. Wrong environment, choose one of those: ${acceptedEnvs.join(
        ", ",
      )}.`,
    );
    process.exit(1);
  }
}
 
// Function that replaces the file content with the right content.
function setEnvironment() {
  console.log(`Setting environmet to ${env}...`);
 
  // String that will override the current export string
  const importerString = `export { env } from './${env}'\n`;
 
  // Env index file location that will be overridden
  const envIndexFileLocation = path.resolve(
    __dirname,
    "..",
    "src",
    "config",
    "env",
    "index.js",
  );
 
  // Writes right content inside the environment file
  writeFile(envIndexFileLocation, importerString);
  console.log(`Environment successfully setted to ${env}.`);
  process.exit(0);
}
 
// Script initialization
validateParams();
setEnvironment();

Usually I place that file inside the scripts folder on the root of my project (where I also have other scripts to automate other parts of my building process).

We have already created the folders structure and the script that will replace everything, now we just need to run it before react-native run-android or react-native run-ios to make sure that the right environment is being used during the process. To do so, you can create some scripts inside package.json:

{
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
 
    "set:env:debug": "node ./scripts/set-env.js --env debug",
    "set:env:staging": "node ./scripts/set-env.js --env staging",
    "set:env:release": "node ./scripts/set-env.js --env release",
 
    "run:android:debug": "yarn set:env:debug && yarn android --variant debug",
    "run:android:staging": "yarn set:env:staging && yarn android --variant staging",
    "run:android:release": "yarn set:env:release && yarn android --variant release",
 
    "run:ios:debug": "yarn set:env:debug && yarn ios --configuration Debug",
    "run:ios:staging": "yarn set:env:staging && yarn ios --configuration Staging",
    "run:ios:release": "yarn set:env:release && yarn ios --configuration Release"
  }
}

To run your project, instead of using run-android or run-ios you should use the scripts that we have created. For example, if I want to run my android app with debug environment, I am going to use yarn run:android:debug.

For Javascript side that would be all. Now you just need to add the variables, like API Url, in each env object and use it in your application.

Setting up Native Environment

As you may know, when working with React Native, some libraries or implementations need both Javascript and Native configuration. Examples of that are Facebook SDK and Firebase. To manage these different configurations on native side, we are going to work with build types for Android and with build configurations for iOS.

Setting up Android Environment

First of all, inside android/app/build.gradle under buildTypes we need to add a new buildType called staging. It will look like that:

staging {
  matchingFallbacks = ['release']
  signingConfig signingConfigs.debug
  minifyEnabled enableProguardInReleaseBuilds
  proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}

Don't forget to add matchingFallbacks there, without it the build may fail. If you want, you can read more about it here.

Now we need to do some extra configuration to allow the staging build type to work properly. Inside the project.ext.react array add the following configuration:

project.ext.react = [
  enableHermes: false,
  devDisabledInStaging: true,
  bundleInStaging: true,
]

That's necessary because, by default, React Native enables development mode and doesn't bundles Javascript in non default buildTypes.

To finish, we need to create a new folder called staging under android/app/src and add a new AndroidManifest.xml file (you can copy the one that's already created inside the debug folder).

Inside that folder you can add all configurations that are necessary for the staging buildType.

That's the way Android knows what the configurations that are going to be used to build your app are. It will look inside the folder with same name as the buildType, and, after that, use the main folder configuration.

Let's say you want to have different app names for each buildType. You can do it by adding a new strings file and overwriting the app_name string. That works with Firebase's google-service.json file too, just add the file under each folder and it will work fine.

Note: Don't forget to properly follow the folders structure. For example, the original strings file is located under main/res/values, so if you want to overwrite it on staging build, you need to create the same directories tree under staging folder.

Setting up iOS Environment

For iOS the logic is almost the same, but with some little differences. Let's dive into 😜?

Open the project in XCode and duplicate the Release build configuration naming it with Staging.

Example of duplicated Release build configuration

Under our iOS project folder, we are going to create a new folder called ConfigFiles and add three .xcconfig files, one for each build configuration. Let’s use the same example that I have used for Android. To have a different app name for each build configuration you can add a new property to each file.

Example of ConfigFiles folder being created

To have access to these properties in your application you need to load them inside each build configuration. We can do that by simply adding it as a ConfigFile on project level.

Example of ConfigFile being loaded inside build configuration

Wait! But how can I use these properties that were loaded into my application?

Just go to your Info.plist file and replace whatever you want with the properties. Following the example, I will replace my old Bundle display name with the variable APP_DISPLAY_NAME.

Example of Info.plist file being updated

Ok… But that just allow us to load different properties and not different files for each build configuration. In case of Firebase, that has a different GoogleService-Info.plist file for each of the configurations, we need to have a different approach. To do that, we can run a script at build time that verifies what is the current build configuration that we are using and replace the necessary files.

I am currently using the following script to do that work for me:

# Name of the resource we're selectively copying
GOOGLESERVICE_INFO_PLIST=GoogleService-Info.plist
 
# Get references to debug, staging and prod versions of the GoogleService-Info.plist
# NOTE: These should only live on the file system and should NOT be part of the target (since we'll be adding them to the target manually)
GOOGLESERVICE_INFO_DEBUG=${PROJECT_DIR}/${TARGET_NAME}/ConfigFiles/Debug/${GOOGLESERVICE_INFO_PLIST}
GOOGLESERVICE_INFO_STAGING=${PROJECT_DIR}/${TARGET_NAME}/ConfigFiles/Staging/${GOOGLESERVICE_INFO_PLIST}
GOOGLESERVICE_INFO_RELEASE=${PROJECT_DIR}/${TARGET_NAME}/ConfigFiles/Release/${GOOGLESERVICE_INFO_PLIST}
 
# Make sure the debug version of GoogleService-Info.plist exists
echo "Looking for ${GOOGLESERVICE_INFO_PLIST} in ${GOOGLESERVICE_INFO_DEBUG}"
if [ ! -f $GOOGLESERVICE_INFO_DEBUG ]
then
    echo "No Debug GoogleService-Info.plist found. Please ensure it's in the proper directory."
    exit 1
fi
 
# Make sure the staging version of GoogleService-Info.plist exists
echo "Looking for ${GOOGLESERVICE_INFO_PLIST} in ${GOOGLESERVICE_INFO_STAGING}"
if [ ! -f $GOOGLESERVICE_INFO_PROD ]
then
    echo "No Staging GoogleService-Info.plist found. Please ensure it's in the proper directory."
    exit 1
fi
 
# Make sure the release version of GoogleService-Info.plist exists
echo "Looking for ${GOOGLESERVICE_INFO_PLIST} in ${GOOGLESERVICE_INFO_RELEASE}"
if [ ! -f $GOOGLESERVICE_INFO_RELEASE ]
then
    echo "No Release GoogleService-Info.plist found. Please ensure it's in the proper directory."
    exit 1
fi
 
# Get a reference to the destination location for the GoogleService-Info.plist
PLIST_DESTINATION=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app
echo "Will copy ${GOOGLESERVICE_INFO_PLIST} to final destination: ${PLIST_DESTINATION}"
 
# Copy over the prod GoogleService-Info.plist for Release builds
if [ "${CONFIGURATION}" == "Release" ]
then
    echo "Using ${GOOGLESERVICE_INFO_RELEASE}"
    cp "${GOOGLESERVICE_INFO_RELEASE}" "${PLIST_DESTINATION}"
fi
 
if [ "${CONFIGURATION}" == "Staging" ]
then
    echo "Using ${GOOGLESERVICE_INFO_STAGING}"
    cp "${GOOGLESERVICE_INFO_STAGING}" "${PLIST_DESTINATION}"
fi
 
if [ "${CONFIGURATION}" == "Debug" ]
then
    echo "Using ${GOOGLESERVICE_INFO_DEBUG}"
    cp "${GOOGLESERVICE_INFO_DEBUG}" "${PLIST_DESTINATION}"
fi

Basically, it searches for the GoogleService-Info.plist files inside my ConfigFiles folder, and replaces it according to the current build configuration.

The most important sections of this script are commented. It was created to replace the Firebase’s GoogleService-Info.plist file, but it can easily be modified to replace other files too.

To run this script, we need to add it to our target Build Phases as a new Run Script Phase:

Example of Run Script Phase being added to target Build Phases

Last but not least, we need to add the GoogleService-Info.plist files under our ConfigFiles directory to allow our script to run properly.

If you have other files that need to be replaced, you can add them in this directory tree too, just don’t forget to modify the current script or create a new one to replace these files.

That’s all folks 🚀

Now, just like magic, you can properly manage multiple environments in your React Native project without thousands of lines of configuration. Just run your project using one of the created scripts and everything will work fine.

If you have any questions or suggestions, please leave a comment. I would be glad in hearing you.

I would like you to check out some other articles about this theme that helped me to write this one:

Thanks for reading!

Other Blog Posts

A experiência de ministrar um treinamento em meio ao isolamento social
A experiência de ministrar um treinamento em meio ao isolamento social

Neste artigo, pretendo falar um pouco mais sobre a experiência de ministrar meu primeiro treinamento, os sentimentos e aprendizados que tive durante esse processo e, também, os principais desafios que foram enfrentados por mim e por toda a equipe envolvida.

Pare de escrever seus componentes React dessa forma
Pare de escrever seus componentes React dessa forma

Após algum tempo trabalhando como desenvolvedor React Native, percebi a tendência das pessoas em estruturarem as sua árvore de componentes com renders condicionais baseados em ifs ternários

How to create a new GitHub Release through Azure Pipelines
How to create a new GitHub Release through Azure Pipelines

Creating a new GitHub release is a common task that a lot of developers are supposed to do during their careers. But on the other hand, it is not as well documented as it could. There are a lot of little tricky things that you will discover only during the process.