You’ve got a React app working locally, but how can you deploy it to different environments?
There’s production, staging, QA, and more… all with their own sets of servers and hostnames and maybe even features that should be enabled or disabled. Plus it still needs to work in development.
Here are a couple ways to do it.
Configure API Endpoints Dynamically
If you can make the assumption that different environments will be accessed by different hostnames in the browser, then this strategy works nicely.
In api-config.js
, do something like this:
let backendHost;
const apiVersion = 'v1';
const hostname = window && window.location && window.location.hostname;
if(hostname === 'realsite.com') {
backendHost = 'https://api.realsite.com';
} else if(hostname === 'staging.realsite.com') {
backendHost = 'https://staging.api.realsite.com';
} else if(/^qa/.test(hostname)) {
backendHost = `https://api.${hostname}`;
} else {
backendHost = process.env.REACT_APP_BACKEND_HOST || 'http://localhost:8080';
}
export const API_ROOT = `${backendHost}/api/${apiVersion}`;
Then in your API file (say, api.js
), you can import the API URL and you’re off to the races:
import { API_ROOT } from './api-config';
function getUsers() {
return fetch(`${API_ROOT}/users`)
.then(res => res.json)
.then(json => json.data.users);
}
Configure Endpoints at Build Time
If you’d rather configure the API endpoints at build time, that works too.
If you’re using Create React App, then you’ll have a global process.env
available to get access to environment variables, including process.env.NODE_ENV
, which will be set to “production” after a build.
Additionally, Create React App will only have access to environment variables named starting with REACT_APP_
, so, REACT_APP_SKITTLE_FLAVOR
works but SKITTLE_FLAVOR
will not.
Here’s how that would look with a Create React App build on a Linux/Mac machine:
$ REACT_APP_API_HOST=example.com yarn run build
# The resulting app would have
# process.env.REACT_APP_API_HOST === "example.com"
# process.env.NODE_ENV === "production"
(Windows handles environment variables differently)
Configure Feature Flags at Build Time
Environment variables can be set to whatever you want. One potential use case is to turn certain features of your app on or off depending on environment. At build time you can do something like this:
$ REACT_APP_SPECIAL_FEATURE=true yarn run build
# The resulting app would have
# process.env.REACT_APP_SPECIAL_FEATURE === "true"
# process.env.NODE_ENV === "production"
Then you could guard parts of your code by checking that variable. This works from anywhere in your app:
function HomePage() {
if(process.env.REACT_APP_SPECIAL_FEATURE === "true") {
return <SpecialHomePage/>;
} else {
return <PlainOldBoringHomePage/>;
}
}
.env Files
Create React App has support for .env files, which means you can put permanent environment variables in one of these files to make it available to the app.
Create a file called .env
and list the variables, one per line, like this:
REACT_APP_SPECIAL_FEATURE=true
REACT_APP_API_HOST=default-host.com
These variables will be loaded in development, test, and production. If you want to set environment-specific variables, put those in files named .env.development
, .env.test
, or .env.production
for the environment you need.
You can read more about how Create React App handles .env*
files here.
Variables are Baked In
At the risk of stating the obvious: the “environment variables” will be baked in at build time. Once you build a JS bundle, its process.env.NODE_ENV
and all the other variables will remain the same, no matter where the file resides and no matter what server serves it. After all, a React app does not run until it hits a browser. And browsers don’t know about environment variables.
Unit Testing
Once you’ve got all these variables multiplying your code paths, you probably want to test that they work. Probably with unit tests. Probably with Jest (that’s what I’m showing here anyway).
If the variables are determined at runtime, like in the first example above, a regular import './api-config'
won’t cut it – Jest will cache the module after the first import, so you won’t be able to tweak the variables and see different results.
These tests will make use of two things: the require()
function for importing the module inside tests, and the jest.resetModules()
function to clear the cache.
// (this could also go in afterEach())
beforeEach(() => {
// Clear the Jest module cache
jest.resetModules();
// Clean up the environment
delete process.env.REACT_APP_BACKEND_HOST;
});
it('points to production', () => {
const config = setupTest('realsite.com');
expect(config.API_ROOT).toEqual('https://api.realsite.com/api/v1');
});
it('points to staging', () => {
const config = setupTest('staging.realsite.com');
expect(config.API_ROOT).toEqual('https://staging.api.realsite.com/api/v1');
});
it('points to QA', () => {
const config = setupTest('qa5.company.com');
expect(config.API_ROOT).toEqual('https://api.qa5.company.com/api/v1');
});
it('points to dev (default)', () => {
const config = setupTest('localhost');
expect(config.API_ROOT).toEqual('http://localhost:8080/api/v1');
});
it('points to dev (custom)', () => {
process.env.REACT_APP_BACKEND_HOST = 'custom';
const config = setupTest('localhost');
expect(config.API_ROOT).toEqual('custom/api/v1');
});
function setupTest(hostname) {
setHostname(hostname);
return require('./api-config');
}
// Set the global "hostname" property
// A simple "window.location.hostname = ..." won't work
function setHostname(hostname) {
Object.defineProperty(window.location, 'hostname', {
writable: true,
value: hostname
});
}