react-redux-universal-hot-example
react-redux-universal-hot-example copied to clipboard
Q: A better way of sharing env-specific variables on both client and server?
I came up with a solution to have the URL prefix of my API configurable, on a per environment basis.
In my case, I'm not using server proxying as in the boilerplate - my app connects directly to my API. This means I need a fully qualified domain name on my requests, whether they happen during SSR, or in the browser.
If we look at the current version of ApiClient.js, we have;
function formatUrl(path) {
const adjustedPath = path[0] !== '/' ? '/' + path : path;
if (__SERVER__) {
// Prepend host and port of the API server to the path.
return 'http://' + config.apiHost + ':' + config.apiPort + adjustedPath;
}
// Prepend `/api` to relative URL, to proxy to API server.
return '/api' + adjustedPath;
}
and in config.js
apiHost: process.env.APIHOST || 'localhost',
apiPort: process.env.APIPORT,
So I want ApiClient to look something like this, which (I think) is the least amount of code I need to change;
function formatUrl(path) {
const adjustedPath = path[0] !== '/' ? '/' + path : path;
// Prepend host and port of the API server to the path.
return 'http://' + config.apiHost + ':' + config.apiPort + adjustedPath;
}
Which won't work client-side since process.env vars are from node.
The solution I came up with was this;
(Note in my code below I combine apiHost and apiPort into 'apiUrl')
in Html.js
<script
charSet="UTF-8"
dangerouslySetInnerHTML={{__html: `window.__apiUrl='${config.apiUrl}'`}}
/>
and in config.js
apiUrl: __SERVER__ ? process.env.APIURL || 'localhost' : window.__apiUrl,
This feels a bit of a hack. The config module writes a variable, which is read by the server code to render the html with a variable, which is read by the config module in the client. Phew.
Is there a better way to share env variables between client & server code?
you can use a webpack plugin which is more elegant I think. So something like
new webpack.DefinePlugin({ 'process.env': Object.keys(process.env).reduce(function(o, k) { o[k] = JSON.stringify(process.env[k]); return o; }, {}) }),
to your webpack config file (the prod.config.js already has something like this).
This will replace ALL env variables so be aware with username/password. I have found this solution here:
http://stackoverflow.com/questions/28717819/environment-variables-in-an-isomorphic-js-app-webpack-find-replace?rq=1
I know much time passed since last answer, but today there's event better way to do it :) : https://webpack.js.org/plugins/environment-plugin/
What if we don't want to build it and instead we publish the bundle that needs to be started on the server? In that case, webpack's environment-plugin doesn't help either.
I'm not 100% sure what you mean but willing to work with you :D What behavior are you looking for or expecting exactly @veeramarni? Depending on what you're after you could possibly make the frontend rely on redux instead of "environment variables" (rightfully, its state management after all) and when the initial redux state is sent from the server it could send environment variables (living on the server) with it. Does that make sense/is it what you're after? Browser side bundle stays "dynamic" and you still get to benefit from environment variables.
Let's say we building Docker images with just the bundle and not copying the sources in that case environment-plugin won't work as we won't be running any build tool.
What we are currently doing is using the following script to pull environment settings to the client.
// List of the env variables you want to use on the client. Careful on what you put here!
const publicEnv = [
'API_URL',
'FACEBOOK_APP_ID',
'GA_ID'
];
const isBrowser = typeof window !== 'undefined';
const base = (isBrowser ? window.__ENV__ : process.env) || {};
const env = {};
for (const v of publicEnv) {
env[v] = base[v];
}
export default env;
Reference: https://stackoverflow.com/questions/28717819/environment-variables-in-an-isomorphic-js-app-webpack-find-replace?rq=1
@veeramarni environment plugin doesn't solve all the problems, it's just a shorter way of using define plugin like this:
new webpack.DefinePlugin({ 'process.env': Object.keys(process.env).reduce(function(o, k) { o[k] = JSON.stringify(process.env[k]); return o; }, {}) })
If define plugin doesn't work for you, the environment plugin won't either.
I know this is old, but I thought I should add my next.js example:
Since next.js renders the ./pages/_document.tsx page on the server side before any other page, this is a good place to do this.
import * as React from 'react'
import Document, { Html, Head, Main, NextScript } from 'next/document'
const UNIVERSAL_ENV_VARS = ['API_URL'] // add all env vars you want to be accessible on the client here, because it's a bad idea to just add the whole process.env to window.process.env
class AppDocument extends Document {
public static async getInitialProps(ctx): Promise<{}> {
const initialProps = await Document.getInitialProps(ctx)
return { ...initialProps }
}
public render(): React.ReactNode {
return (
<Html>
<Head>
<style>{`body { margin: 0 } /* custom! */`}</style>
<script
charSet='UTF-8'
dangerouslySetInnerHTML={{
__html: `window.process={ env: ${JSON.stringify( // set window.process.env to the narrow list of env vars
UNIVERSAL_ENV_VARS.map(key => ({
key,
value: process.env[key]
})).reduce((acc, curr) => {
acc[curr.key] = curr.value
return acc
}, {})
)} }`
}}
/>
</Head>
<body className='custom_class'>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default AppDocument
I decided to use window.process.env so I can use the same code on client and server without the checks.