Updated December 14, 2016
Managing Configuration in React Native
Originally publish on medium.com.
Why the pineapple though?
You’re a developer. You build apps. You probably build them in an iterative way —build something, test it, realize it doesn’t work, try something else, realize that definitely doesn’t work, and repeat until you get to a solution that works well, reads well, and can be maintained well.
In that iterative process you don’t want to be writing to your production database and messing with the data. You probably also don’t want to be using up your API quota testing things out.
In a situation like this you’re probably going to be changing config variables… Does this look familiar?
config.js
const SERVER_URL = 'http://localhost:3000'; // dev
// const SERVER_URL = 'https://my-staging-server.com'; // staging
// const SERVER URL = 'https://my-prod-server.com'; // production
const GOOGLE_API_KEY = '1234'; // dev
// const GOOGLE_API_KEY = '5678'; // staging
// const GOOGLE_API_KEY = '9101'; // production
export default {
SERVER_URL,
GOOGLE_API_KEY,
};
Whenever you’re switching environments you’ve got to make sure you uncomment the right combination of code otherwise everyone is in for some confusion? I’m guilty of this.
react-native-config
Obviously we don’t want to continue to do that. That’s where react-native-config comes into play. It allows us to set up different configuration files for different environments. It advocates for twelve-factor configuration management which, summed up, is:
Apps sometimes store config as constants in the code. This is a violation of twelve-factor, which requires strict separation of config from code. Config varies substantially across deploys, code does not.
A litmus test for whether an app has all config correctly factored out of the code is whether the codebase could be made open source at any moment, without compromising any credentials.
So with that in mind we know that our SERVER_URL and GOOGLE_API_KEY should not be in our code. I really like the idea of keeping my app ready to open source at any time.
Usage
So what can we actually do with react-native-config? We can store all of our configuration variables in one place and access them from anywhere — our Javascript, Objective-C/Swift, or Java. No need to duplicate things for different environments. This is React Native — we (try to) do it once and make it work for all the supported platforms.
Since this works in JS and native code for changes to be reflected you’ll have to recompile your app. This is kind of a bummer but really, how often do you change configuration variables?
Once the package is installed and configured (we’ll cover that later) you create a .env file in the root of your project. I typically create a .env.dev, .env.staging, and .env.prod to represent my different environments.
Once you have that done it’s super simple to access any of your configuration variables from anywhere in your app.
In Javascript
app.js
import Config from 'react-native-config';
Config.SERVER_URL;
Config.GOOGLE_API_KEY;
In Java
app.java
public HttpURLConnection getApiClient() {
URL url = new URL(BuildConfig.SERVER_URL);
}
and in Objective-C
app.m
#import "ReactNativeConfig.h"
NSString *serverUrl = [ReactNativeConfig envFor:@"SERVER_URL"];
(for more examples check out the official docs)
Installation
Since we’re able to access these configuration variables in our Javascript code as well as the native code our installation is a little more involved. But, being React Native, it’s still pretty simple
$ npm install react-native-config --save
$ react-native link react-native-config
Configuring iOS
Open your .xcodeproj. You’ll need to enabled a few things from within Xcode
- Go to your Build Settings tab, search for “preprocess”, and set Preprocess Info.plist File to Yes.
- Set Info.plist Preprocessor Prefix File to ${BUILD_DIR}/GeneratedInfoPlistDotEnv.h
- Set Info.plist Other Preprocessor Flags to -traditional
It should end up looking something like that.
Now that we’ve got everything configured let’s actually hook things up. We’ll create a new scheme to go along with each of our environments, so Dev, Staging, and Production. We’ll just set up one now because the steps are the same for each.
In Xcode, next to the play & stop buttons, select your current scheme and then select “Manage Schemes”.
With the window open, select your current scheme then select the gear icon, then “Duplicate.”
Protip: Make sure that “Shared” is selected for your new schemes so that any other developers working on your project don’t have to go through this step.
Name your new scheme (I’m naming mine Dev). Then expand “Build” and click “Pre-actions”. Click the plus sign and then “New Run Script Action”.
Finally you wan to paste the following script into the box. Replace .env.dev with the appropriate file for the environment.
echo ".env.dev" > /tmp/envfile
You’re all set up! Now you can do the same for your staging and production environments. You can choose the scheme when you’re running the app or building it.
Configuring Android
We need to add one line to android/app/build.gradle, right after any other “apply”
android/app/build.gradle
apply plugin: "com.android.application" /\* This should already exist \*/
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
With that set up we’re good to go! But, much like on iOS, I want to preconfigure things as much as I can. I’ll do something similar to what I did for iOS but we’ll be leveraging npm scripts instead of schemes. For our 3 different environments we’ll set up 5 scripts — 3 to run our app in dev mode and 2 to build for staging and production. The first 3 scripts are to run the app in development. We’re setting an environment variable to tell react-native-config which file to reference and running it like normal. Using a script in our package.json just makes it easier.
package.json
"scripts": {
"android-dev": "ENVFILE=.env.dev react-native run-android",
"android-staging": "ENVFILE=.env.staging react-native run-android",
"android-prod": "ENVFILE=.env.prod react-native run-android",
},
Building is going to be pretty much the same, just a bit more verbose. I’m only going to do one (for production) but you’ll get the point. First we export the correct file, then cd into our android/ directory, then assemble the release.
package.json
"scripts": {
// ...
"build-android-prod": "export ENVFILE=.env.prod && cd android && ./gradlew assembleRelease && cd .."
},
Boom! You’re all set up. You would then take the resulting .apk and distribute it.
Workflow
You’ve probably got a sense of my workflow already but I wanted to run through it with you because I feel like writing still.
So what’s my day-to-day workflow look like with this set up?
When working with iOS, I’ll open up the project in Xcode, select the correct scheme (95% of the time it’s Dev), press play and then I’m all set to go. I connect to my local server (Meteor in my case) and use development keys. When it’s time to deploy I’ll go back to Xcode, choose the appropriate scheme (Staging or Production), choose “Generic iOS Device”, then go to Product > Archive and follow the normal steps!
For Android, I run my production script (npm run build-android-prod
), wait for everything to finish, grab the newly generated .apk, and upload it to the Google play store.
This is a simple and consistent way to handle the configuration needs in your app. Setting it up and adjusting your workflow is easy and achievable. Do it today (and recommend this article so others can handle their RN config in a silky smooth fashion)!