Intro
We recently expanded our service offering at Whitelabel to include mobile apps using Expo and React Native. This technology allows us to develop native applications for both iOS and Android devices using our knowledge of JavaScript and React. As with any new framework or language, there was a learning curve, specifically when it came to our project workflow.
While our team was familiar with how to manage environment variables on an array of web projects (configuring variables such as API urls, keys, etc. for local, staging, and production environments), Expo and React Native do things slightly differently.
After some trial, error, and research, we created a workflow based on a post from Peter Piekarczyk. Beginning with creating an Expo-based equivalent to a .env file
, we’ll show you our team’s full workflow when it comes to using React Native and Expo’s Release Channels for local, staging, and production environments.
What is Expo?
First, I want to clarify what Expo is and why we use it.
“Expo is a set of tools built on top of/around React Native. These tools depend on one key belief held at Expo: it’s possible to build most apps without ever needing to write native code, provided that you have a comprehensive set of APIs exposed to JavaScript.” (source)
It’s basically Rails for React Native. It allows our team to write native applications using JavaScript in whatever code editor we want rather than having to use Xcode or Android Studio (and writing native code). Not only that, but it “provides access to device and system functionality such as contacts, camera, and GPS location” from Javascript.
If you ever want to dive into native code, you can always “eject” from Expo, but we’ve never needed to do that and we actually try to avoid it whenever possible – Expo just provides so much that it’s rarely worth ejecting.
If you’re interested in diving deeper, there is some great documentation on Expo’s site including an info on the differences between Expo and React Native.
Environment Variables in Expo using Release Channels
What isn’t entirely clear when getting started with Expo is how to configure environment variables. You may be used to creating .env
files to define your API url or other variables for local, staging, and production environments, however Expo doesn’t use .env
’s by default.
In fact, without external packages and/or ejecting from Expo, the primary means of creating environments with their own variables is by using Expo’s Release Channels. These release channels allow you to send out different versions of your application to your users by giving them a URL or configuring your standalone app.
Here’s how we use release channels to manage our environments:
Start by Creating the Environment Variable File
We start by creating an environment.js
file that is also added to our .gitignore
(so that any sensitive information is never published to GitHub). This environment.js
essentially serves as the project’s .env
file, allowing us to store api urls and other variables that change based on the app’s current environment.
**Quick Disclaimer:** You should assume that anything you put into this file could potentially be discovered by an end-user, so these variables should all be public. If there are real secrets that you don’t want anyone to see, such as database passwords for example, you should store these on a server and have them returned to the app after an API call.
Storing the Variables: environment.js
==============================================
/*****************************
* environment.js
* path: '/environment.js' (root of your project)
******************************/
import { Constants } from "expo";
import { Platform } from "react-native";
const localhost =
Platform.OS === "ios" ? "http://localhost:8080" : "http://10.0.2.2:8080";
const ENV = {
dev: {
apiUrl: localhost,
amplitudeApiKey: null,
},
staging: {
apiUrl: "https://your.staging.api.here.com",
amplitudeApiKey: "[Enter your key here]",
// Add other keys you want here
},
prod: {
apiUrl: "https://your.production.api.here.com",
amplitudeApiKey: "[Enter your key here]",
// Add other keys you want here
}
};
const getEnvVars = (env = Constants.manifest.releaseChannel) => {
// What is __DEV__ ?
// This variable is set to true when react-native is running in Dev mode.
// __DEV__ is true when run locally, but false when published.
if (__DEV__) {
return ENV.dev;
} else if (env === 'staging') {
return ENV.staging;
} else if (env === 'prod') {
return ENV.prod;
}
};
export default getEnvVars;
==============================================
You’ll see a couple of things here. First off, we check what platform the app is being run on (iOS or Android) via Platform
to determine the correct localhost
address. We quickly found that Android requires a different address in order to correctly render images and other static assets.
Next, the ENV
object is where we add variables including each environment’s apiUrl
and, in this case, the key for Amplitude which we used for analytics tracking on each environment.
Lastly, the getEnvVars()
function is what’s exported from the file. It checks which release channel the app’s compiled binary is running via Expo’s Constants
and returns the corresponding variables.
How to Access Your Environment Variables
After the variables are set up, we import the getEnvVars()
function from environment.js
in our api.js
file in order to access the proper apiUrl
for our api calls:
/*****************************
* api.js
* path: '/utils/api.js'
******************************/
// Import getEnvVars() from environment.js
import getEnvVars from '../environment';
const { apiUrl } = getEnvVars();
/******* SESSIONS::LOG IN *******/
// LOG IN
// credentials should be an object containing phone number:
// {
// "phone" : "4191231234"
// }
export const logIn = (credentials, jwt) => (
fetch(`${apiUrl}/phone`, {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + jwt,
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials)
})
);
You can see how useful this can be with multiple variables within the same environment. To import a key for Amplitude into its own config file, for example, we do the following from Amplitude.js
. You’ll also see that in the initialize()
function, if they key hasn’t been set in our environment file, we throw an error.
/*****************************
* Amplitude.js
* path: '/utils/analytics/Amplitude.js'
******************************/
// Import getEnvVars() from environment.js
import { Amplitude } from 'expo';
import getEnvVars from '../../environment';
const { amplitudeApiKey } = getEnvVars();
...
const initialize = () => {
if (!amplitudeApiKey) {
throw new Error(“Amplitude key isn’t set”);
}
Amplitude.initialize(amplitudeApiKey);
isInitialized = true;
};
...
A Suggested Workflow
After a few projects, we’ve come up with an Expo workflow that closely mirrors that of our web development projects. While not 1-to-1, hopefully this suggested workflow can help you and your team on your own Expo projects.
Dev: Local Development
Run locally on Apple Simulator, Android Studio, Expo Mobile App
The development environment includes:
- Running the Expo app locally using Apple Simulator or Android Studio
- Running via the Expo mobile app
In development, run your app on Apple Simulator or Android Studio. The latter takes a little bit more set up.
To run your app, run:
expo start
And then select which simulator (iOS or Android) you want to open the app in:
With iOS, I’ve found you can run the command with the Simulator app closed or already open, while with Android, things are smoothest if you already have the AVD (Android Virtual Device) running.
By running locally, the __DEV__
flag will be set to true and your ENV.dev
variables will be used.
Staging: TestFlight (iOS) & Testing Tracks (Android)
Build an app binary file with the “staging” release channel for upload to a testing environment
The staging
environment includes:
- Running the app via TestFlight or an Android testing track
There are essentially two main components here:
- The initial build using the proper release channel
- Publishing updates to the proper release channel
First, you need to build a binary that you can upload to App Store Connect (iOS) and Google Play Console (Android) with your release channel set to staging
.
expo build:ios --release-channel staging
or
expo build:android --release-channel staging
These will build the proper binary (.ipa for iOS and .apk for Android) that you can upload. For more info on uploading to app stores, check out the Expo docs.
After that, you won’t need to rebuild and upload a new binary unless you make core changes such as updating the Expo SDK or modifying other configurations in your app.json
(you can read more on that here).
Whenever you do want to publish updates, you can run:
expo publish --release-channel staging
Any changes you’ve made will be published to the staging
release channel and be available through TestFlight or your Google Test Track since your binary was built and set to that channel.
Production: App Store (iOS) & Google Play (Android)
Build a binary with “prod” release channel for upload to public app stores
The production
environment includes:
- Running the app via the public download from the Apple App Store or Google Play
Lastly, when you want to go live, building and publishing is just like that of the staging environment, however you designate your release channel as “prod”:
Building binary for upload:
expo build:ios --release-channel prod
Or
expo build:android --release-channel prod
Publishing updates
expo publish --release-channel prod
Digging Deeper: Advanced Release Channels
If you want to be even more of an Expo wiz, take a look at the documentation on release channels. Expo provides the option to promote and roll back channel entries between release channels – which may save you some build time in the long run.
Conclusion
Using Expo and Expo Release Channels, our team has built a workflow that works for us. The environment.js
file serves as our normal .env
, TestFlight/Google Play Test Tracks take over as our “staging” environment, and the released app is our final “production.”
It should be said that there are alternatives to this workflow, including packages like react-native-config and babel-plugin-inline-dotenv. However, the former requires ejecting from Expo in order to “link” to native code, and the latter requires additional configuration.
Using release channels allows us to create pseudo environment variables and take advantage of a feature already provided by Expo. We hope this explainer helps you and your team. Let us know what you think!