-
-
Notifications
You must be signed in to change notification settings - Fork 27k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Compiled config vs runtime config #578
Comments
An ad hoc solution would be to put some placeholder into HTML and then replace it with a script tag initializing a global before serving the app. This is however outside of CRA concerns IMO. |
I got directed to this thread via Twitter, so need to qualify my input with I've limited experience re best practice for React & single page apps. My interest is around 12factor/Heroku specific aspects. So looking at the problem as it pertains to Heroku implementation of 12factor, the issue is really that the notion of a
Could that be encapsulated into some higher order and reusable concept like
|
The Twelve-Factor App guide mentioned by @mars has a section about "build, release, run" cycle. It explains that the build stage is only about creating an executable that could run anywhere. It is the release stage that should be responsible for combining this executable with the the deployment environment configuration.
Indeed, if merged, this PR #1489 will relieve apps not using the HTML5 Then, the only remaining task to make One of the solutions to add support for runtime environment variables that is really simple and lightweight would be to add an
// This file will not end up inside the main application JavaScript bundle.
// Instead, it will simply be copied inside the build folder.
// The generated "index.html" will require it just before this main bundle.
// You can thus use it to define some environment variables that will
// be made available synchronously in all your JS modules under "src".
//
// Warning: this file will not be transpiled by Babel and cannot contain
// any syntax that is not yet supported by your targeted browsers.
window.env = {
// This option can be retrieved in "src/index.js" with "window.env.API_URL".
API_URL: 'http://localhost:9090'
}; The <body>
<div id="root"></div>
<!--
Load "env.js" script before the main JavaScript bundle.
You can use it to define "runtime" environment variables.
Please open this file for more details.
-->
<script src="%PUBLIC_URL%/env.js"></script>
</body> After running Should I submit a PR documenting this technique or even adding this |
@tibdex I like this solution, but did you test via the npm run test with the jest. seems will have some error and it can not get in the test level. any advice? |
Writing configuration values into a file is antithetical to 12-factor. Instead, always use runtime environment variables for configuration. create-react-app [CRA] does not provide a production runtime. It only builds a static bundle. Runtime environment is not something solvable by CRA, unless it becomes prescriptive about deployment which is unlikely given its wide range of applications. Solving this problem is about finding a way to set those variable values in the React app at runtime instead of buildtime. I know of two ways to accomplish this:
This is a challenging topic 🤓 Bravo for trying to solve it 👏👏👏 |
I've been working on a somewhat related effort to manage the configuration/credentials at runtime problem. And I'd be interested in helping come up with a standard approach of doing it for SPAs like this. The basic approach is:
If we can reach agreement on what the API response should look like, how to define what the env var API host/url should be, and how/where it should be exposed I'd be happy to do the heavy lifting and try to write a shim. I can foresee some API-like wrapper that lets people store their secrets direct on S3 and the API just exposes them in the right format. What do y'all think? |
@glenngillen from my perspective, configuration should be in-place when the javascript app loads in the browser. Requiring remote fetch of config values from an external service:
The fundamental problem is that SPAs can be deployed many many different ways: static web sites, GitHub sites, instant deploy services (Heroku, Now, Netlify, etc), embedded in blogs (Wordpress, Drupal), composed with web app servers (Node, Rails, Django, etc), and the runtime is where this problem must inevitably be solved. |
@mars agreed, but... as you've mentioned "runtime" in this context includes things like static web/hosting directly off s3 and GitHub pages. Short of fetching config on demand I see no alternative for these apps other than pushing the config back into the build/compile phase. |
@tibdex Finally I get the unit test code fixed. Basically we have to mock the window.env in the jest mock folder |
@jayhuang75, I think a better place to provide the configuration values to the tests might be @mars, I agree that ideally the config should not be stored in a file. The |
@tibdex your technique of adding a Perhaps using a module-based approach like I did for Heroku runtime env would allow you to capture logic for dev/test vs production runtime and graceful fallback/error-messaging when the env is not setup correctly? |
I am having the same problem. I need to change a the index.html based on a dynamic value from the database. This is easy to do when serving a prebuilt CRA via NodeJS with a simple string search and replace, but becomes impossible in dev mode without gutting the CRA inside out. The more I think about it I do not see an easy way for doing this dynamically in both environments. Any plans to support this or any ideas how to implement it with the least CRA changes possible? |
@tibdex you mean something along the lines of this (regarding the server on which the static bundle is deployed)? Seems to be working allright :-) const express = require('express');
const path = require('path');
const serialize = require('serialize-javascript');
const env = {
'API_URL': process.env.API_URL,
'DYNO': process.env.DYNO || 'Not running on a dyno',
}
const app = express();
app.use(express.static(path.join(__dirname, 'build')));
app.get('/env.js', function (req, res) {
res.set('Content-Type', 'application/javascript');
res.send('var env = ' + serialize(env));
});
app.get('/*', function (req, res) {
res.sendFile(path.join(__dirname, 'build', 'index.html'));
});
app.listen(process.env.PORT); |
@rmoorman this would work indeed when you use a custom express app to serve your |
@tibdex thank you for the clarification. The small example is indeed meant for the case when one can (and wants to) use a custom express app for serving the static files (the same could also be achieved using something else for the app part of course). |
@tibdex Indeed it would but it wouldn't be 12-factor, if that's important to someone, because it is technically using a static config file. In other words, it solves the problem of moving the config out of the build, but it doesn't solve the problem of injecting the config from the environment. That is going to require a system like Node that can read from the environment (maybe there are ways to do this in NGINX, but I'm not sure). |
@neverfox or for anyone else on this thread.... Maybe we need to take a step back in regards to why a config file is not recommended for 12-factor. I could be wrong, but my understanding is that they are referring to a server side environment. Let's review the goals:
If you create a service say on a subdomain (meaning it doesn't have to be hosted with the static build directory), and it all does it serve responses for the environment config (/env.js) AND that server handler simply looks up configuration from environment variables, I feel you are achieving the goals mentioned above. What's important is the server side is not using a config file. This allows the server to easily change settings w/o relying on source code change / new deploy. There are also no checked in environment configs in the repo. From the client side you wouldn't be able know the difference. IMO, it passes the 12-factor app guidelines. My only leftover concern is relying on global variables. It's possibly for other 3rd party scripts to be on your page that could alter these values (This might be an issue for some who use tag manager scripts that load more 3rd party scripts onto your site). You could also use something |
I'll close as I don't see an actionable item for us here. If you disagree please file a new issue. I feel like a part of this discussion is a bit too philosophical. We shouldn't need to blindly apply the guidelines somebody else wrote, but to think critically. Building client-side apps is not the same problem as building server-side apps, and some invariants are just not true (e.g. there are no "secrets" in client-side apps). As for injecting runtime variables, I feel like doing this via some sort of templating is reasonable and I think it falls out of scope of CRA itself which isn't concerned with what you do after deployment. |
@gaearon regarding this comment #578 (comment), I'm agree to do this to manage environment variables, but it won't work for the |
I don't see what else you could do. The tradeoff is:
Option 1 is default in CRA. Option 2 is what you get if you specify What am I missing? Is there a third option here? |
@gaearon you're right. But the javascript part can depend on a global variable which can be easily overridden in a higher level instead of replacing In other languages, in most of the cases, you always can pass environment variables in runtime. |
I settled for a solution based on this post by @tibdex where in my case a script
config.js stores the URL settings in browser sessionStorage, see w3.schools doc making the values accessible globally by Javascript throughout the app once launched, with browser storage lifetime for the current session. |
@tibdex curious if you know of an equivalent solution for react-native (no index.html)? I'm using react-native-web, so the script imported config works for the web build, but wouldn't be viable for native builds. |
Nope, I have never used |
Topic: support for runtime environment, e.g., SITE_URL and such The issue is that Webpack has already packed everything, meaning that there is no way to use API_URL as I used it. Links: https://12factor.net/ https://github.com/mars/create-react-app-buildpack#compile-time-vs-runtime vuejs/vue-cli#1217 facebook/create-react-app#578 https://stackoverflow.com/questions/45111936/passing-environment-variable-to-webpack-2-after-build
I landed on the exact solution above about having config variables written into a dynamic /environment.js file. One addition I made is to patch these onto webpacks process.env at runtime so that the config can still be part of the build and simply overwritten be remote configuration. /**
* Remote configuration can be attached from window.process.env
* Take the remote config and patch it onto webpacks process.env
* This is a runtime side effect that should be run before anything else
*/
const patchLocalConfig = (
localConfig: { [key: string]: ?string },
remoteConfig: ?{ [key: string]: ?string },
debug?: boolean,
) => {
if (!remoteConfig) {
if (debug) {
console.warn(
'No remote config found. Set window.process.env to an object to apply a remote configuration.',
);
}
return;
}
if (debug) {
console.log('Remote Config:', JSON.parse(JSON.stringify(remoteConfig)));
console.log('Local Config:', JSON.parse(JSON.stringify(localConfig)));
}
Object.entries(remoteConfig).forEach(([key, value]) => {
if (!(key in localConfig)) {
throw new Error(
`Remote configuration var '${key}' was not found in local configuration.`,
);
} else if (
typeof value === 'string' ||
value === null ||
value === undefined
) {
localConfig[key] = value;
}
});
if (debug) {
console.log('Merged Config:', JSON.parse(JSON.stringify(localConfig)));
}
}; And to use it, just make sure to run it at the top of your main entry point before the rest of your code runs. const remoteConfig =
'process' in window &&
window.process.env &&
typeof window.process.env === 'object'
? window.process.env
: null;
patchLocalConfig(
process.env,
remoteConfig,
true,
); |
@rmoorman Do you see any issues with using essentially an endpoint for that env.js? Since it is loading it async but once its loaded it exists through the entire app right? |
@FahdW what do you mean with endpoint? An http handler which serves up the As mentioned before, I would indeed opt for an But I cannot judge what would be the best fit for your situation. Generating the Either way, the Lastly, if there are a lot of dynamic configuration options to consider for your app, or they change a lot through the apps lifetime within a browser session, it could also be a good thing to have some (REST/graphQL/whatever) API satisfy the react app's needs. |
@rmoorman I was wondering more along the lines of you generate your express env.js endpoint. Meaning your webapp needs to read from it, isn't this read async? Does it only happen once, i am just trying to understand the ramifications of serving env.js. So far i have not seen any issues with my webapp getting the values, but just wanted to know of any potential side effects that may occur due to fetching env.js from the express server that our webapp is deployed with. I believe the env.js is an async action but once the webapp responds with it, it persists forever? |
@FahdW I have this setup right now; this is how it works. When releasing an artifact to an environment, we create the env.json from values managed in AWS Parameter Store and deploy it sibling to the app (served from S3). You're right that requesting the config is a second async request triggered after the app starts. To ensure nothing depending on config values is ran too soon all modules with setup/initializing code have an init method that is called from a Startup module after config.json has been received. This is triggered by wrapping the root React component with a config fetcher that simply initiates the request for config.json, then mounts its children and calls the startup module once config.json is received. |
@FahdW I now have a better picture of what your question was about. In case of using a javascript file for the environment variables (
which ends up like this in the build output
Loading the You could however (as @forrest-rival also pointed out) use an asynchronous request to fetch some json ( I have been using both approaches in the past and personally, I found a synchronously loaded env.js file to be sufficient a lot of times. Loading the configuration dynamically gives you some flexibility with regards to configuration loading but comes with a bit complexity. In any case, at some point you might have to consider what to do with the app loaded up in your user's browsers when configuration changes. How to signal configuration changes to the browsers? Will you poll for changes? Have a socket open? SSE? And in case your configuration changed, is it enough to just load the new values or do you need to reload the whole application? @forrest-rival, nice idea to use aws parameter store and s3 for this :-) |
💚 The custom environment variables feature.
Update February 2017: We devised a solution for runtime config on Heroku. create-react-app-buildpack now supports runtime environment variables by way of injecting values into the javascript bundle at runtime. This is still an open question for other runtimes and deployment techniques.
This feature leverages Webpack's DefinePlugin which is explained as:
The issue is not all configuration should be compiled into an application.
Things that are stable between environments (dev, staging, production) of an app make sense to compile-in:
Things that change between environments would be better provided dynamically from the current runtime:
Ideally a bundle could be tested in CI, then promoted through environments to production, but compiling all of these values into the bundle means that every promotion requires rebuild.
I've used Node to serve single-page apps in the past with a base HTML template that gets environment variables set via inline
<script>
at runtime; even used a server-side Redux reducer to captureprocess.env
values into an initial state.Now, I'm trying to find a good solution to the conflation of runtime configuration values being compiled into the bundle.
create-react-app
is clearly focused on producing a bundle that can be used anywhere.Is this project open to solutions/hooks for injecting runtime variables, or is this wandering into
npm run eject
territory?The text was updated successfully, but these errors were encountered: