I'm aware that secrets shouldn't be leaked in the browser (https://github.com/zeit/next.js/issues/107), but it would be good to have access to some environment variables, e.g a remote graphql endpoint URL in my case. How can environment variables be accessed? A naive process.env.MY_VAR
doesn't work.
As far as I tested, it works fine.
// package.json
{
"scripts": {
"start": "MY_VAR=hi next",
}
}
``` .js
// pages/index.js
const env = 'undefined' !== process ? process.env.MY_VAR : null
export default class extends React.Component {
static getInitialProps () {
return { env }
}
render () {
return
I did it like above.
MY_VAR=hi npm start
also worked fine.
OK I'm new to SSR and just realized my need is more complicated. I need to access a GRAPHQL_ENDPOINT
env var which can then be used to create an ApolloClient
as shown here: https://github.com/zeit/next.js/issues/106#issuecomment-257099847
So this environment variable needs to be accessible on both client and server code.
Sorry it's not related to Apollo etc, but is because I wrap a component in a higher-order component (something also new to me). I don't know how to access the variable from the wrapped component. The following renders an empty page instead of "hi":
// package.json
{
"scripts": {
"start": "MY_VAR=hi next",
}
}
// components/Hoc.js
import React from 'react';
export default function hoc(WrappedComponent) {
return class Hoc extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
};
}
// pages/index.js
import React from 'react';
import hoc from '../components/Hoc';
const env = process.env.MY_VAR;
class Index extends React.Component {
static getInitialProps() {
return { env };
}
render() {
return <div>{this.props.env}</div>;
}
}
export default hoc(Index);
// above doesn't work, but `export default Index;` renders "hi" properly
@sedubois you can globally access process
from server, so I think it's not relevant whether if component is higher-order or not.
For accessing environment variables from client, you can embed them to DOM and retrieve later like the following.
const { MY_VAR } = 'undefined' !== typeof window ? window.env : process.env
export default () => {
return (
<div>
{MY_VAR}
<script dangerouslySetInnerHTML={{ __html: 'env = ' + escape(JSON.stringify({ MY_VAR })) }}/>
</div>
)
}
Actually, your real code would become more complicated to not render script
tag again and again tho.
@sedubois Because the Index
component is wrapped in the Hoc
, next will not call getInitialProps()
on Index
. It will only call that method for the top-most component. To solve this problem you can implement getInitialProps()
within Hoc
, and return the result from Index
's method.
// components/Hoc.js
import React from 'react';
export default function hoc(WrappedComponent) {
return class Hoc extends React.Component {
static getInitialProps(ctx) {
return WrappedComponent.getInitialProps(ctx)
}
render() {
return <WrappedComponent {...this.props} />;
}
};
}
@dstreet Your answer seems correct! Thanks.
Feel free to reopen if you still have the problem.
There's also https://github.com/mridgway/hoist-non-react-statics which is in general a good idea when creating HOCs.
@nkzawa I don't know how to access the env var in the _browser-side_ getInitialProps
. (Needed when navigating to the page instead of loading it directly.)
It's in getInitialProps
that I configure my Apollo client (both on server and browser):
https://github.com/sedubois/realate/blob/master/containers/Apollo.js#L74
Could we just add this to the Webpack config?
new webpack.DefinePlugin({
'process.env.XXX': JSON.stringify(process.env.XXX),
'process.env.YYY': JSON.stringify(process.env.YYY),
}),
The XXX/YYY should be e.g configured in the package.json. Then the environment variables will be substituted for their values and therefore accessible browser-side?
If you agree I can _try_ to add that to the Webpack config...
Or maybe just wait for https://github.com/zeit/next.js/pull/174, then I can add it myself. Although once again I wouldn't wish other users to get into the same surprises and need to figure all of this out.
@sedubois you can embed env vars to dom like https://github.com/zeit/next.js/issues/159#issuecomment-257260817 .
DefinePlugin
wouldn't be suitable in this case since it replaces variable references to other values in your code which causes to expose your env vars.
For the moment I managed to do it using a HOC and using it on every page. Probably would be nicer with _layout.js
support and using context for passing env vars.
// withApiHoc.js
import React from 'react';
import Axios from 'axios';
const endpoint = process.env.API_ENDPOINT;
export default (WrappedComponent) => {
return class extends React.Component {
static getInitialProps(ctx) {
let props = {};
if (WrappedComponent.getInitialProps) {
props = { ...WrappedComponent.getInitialProps(ctx) };
}
return {
...props,
endpoint,
}
}
render() {
return (
<WrappedComponent
{...this.props}
api={
Axios.create({
baseURL: this.props.endpoint + '/api/v1/',
})
}
/>
);
}
};
}
this will only work in the first request, right? because only the first one is server rendered and has access to process.env
@luisrudge yep, so you have to embed env vars among all pages and enable to access these data when navigating on client.
DefinePlugin wouldn't be suitable in this case since it replaces variable references to other values in your code which causes to expose your env vars.
@nkzawa you're also exposing the env var to the browser when embedding it in the dangerouslySetInnerHTML
(and then in your example you're rendering it so it's even more visible). I don't see how DefinePlugin
makes things worse. My GraphQL endpoint URL needs to be known to the browser, as it runs Apollo which queries the endpoint directly. And as you just mentioned, it doesn't solve the issue of needing to add this script code on all pages, whereas DefinePlugin
would normally solve this?
Also, although it renders properly I got console errors when trying to run your code:
Uncaught SyntaxError: Unexpected token %
...
Uncaught TypeError: Cannot read property 'MY_VAR' of undefined(…)
(last error is thrown by the very first line of code)
@sedubois ah yeah, true. You can use DefinePlugin
if you want after supporting custom webpack config.
I'm not sure where %
come from. How did you implemented escape
? You'd like to use something like https://github.com/zertosh/htmlescape .
My example code is just for showing the idea. Please fix it for your use.
@nkzawa thanks, I'll just wait for the configurable Webpack for now.
The HOC wasn't something I wanted (i wanted to extend a layout instead, for an unrelated reason).
I ended up with this... basically the componentWillMount
only runs on the client the first time when the page loads... sets up all the related analytics classes (i'm still using window.analytics
because lazy).
import React from 'react';
import 'isomorphic-fetch';
import segment from '../lib/segment';
class PageLayout extends React.Component {
static async getInitialProps ({ req }) {
if (req) {
const configUrl = process.env.CONFIG_URL;
const res = await fetch(configUrl);
const data = await res.json();
return data;
}
return {};
}
componentWillMount () {
if (typeof window !== 'undefined') {
segment(); // my analytics snippet
const analytics = window.analytics;
if (analytics.load && this.props.segmentKey) {
analytics.load(this.props.segmentKey);
}
analytics.page(); // marks a pageview, runs every time.
}
}
}
PageLayout.propTypes = {
segmentKey: React.PropTypes.string,
};
export default PageLayout;
Does this make sense? Would love feedback and more importantly, if you're using this approach, please let me know. 😄
I think you could also inject the variables into the window object / into a custom dom node / into something right as the componentWillMount
runs, and it'd only run the first time if this.props.whateverKey
exists.
In Next 2.0, you can do something like this in your next.config.js
:
const webpack = require('webpack');
module.exports = {
webpack: (cfg) => {
cfg.plugins.push(
new webpack.DefinePlugin({
'process.env.CUSTOM_VALUE': JSON.stringify(process.env.CUSTOM_VALUE),
})
);
return cfg;
},
};
Since Next runs Webpack to build the code before running it on the client and server, the JS will be embedded with the env values from build-time.
_(Having to require('webpack')
felt a bit odd, so I created a PR to pass the webpack
module by reference to the config function: https://github.com/zeit/next.js/pull/456)_
@ericf Is this still in the works? I am using the exact same snippet you posted and don't see any vars under process.env
in the browser. Next version 2.0.0-beta.4.
@purplecones yeah it has been with the latest beta. Did you make sure to run your build with the env vars set? I'm using the dotenv
package and I'm loading my local .env
file inside next.config.js
.
@ericf I also can't seem to get this to work on beta.5. Identical webpack config to what you included
@purplecones @jbaxleyiii you should console.log(process.env)
in your next.config.js
to make sure it has everything. I'm using dotenv
to load from my .env
file during development. My next.config.js
actually looks like this:
const webpack = require('webpack');
if (process.env.NODE_ENV !== 'production') {
require('dotenv').config();
}
module.exports = {
webpack: (config) => {
config.plugins.push(
new webpack.DefinePlugin({
'process.env.FB_APP_ID': JSON.stringify(process.env.FB_APP_ID),
'process.env.FB_PAGE_ID': JSON.stringify(process.env.FB_PAGE_ID),
})
);
return config;
},
};
@ericf I used ur exact webpack config.
const webpack = require('webpack');
if (process.env.NODE_ENV !== 'production') {
require('dotenv').config();
}
module.exports = {
webpack: (config) => {
config.plugins.push(
new webpack.DefinePlugin({
'process.env.GRAPHQL_URL': JSON.stringify(process.env.GRAPHQL_URL),
})
);
return config;
},
};
The variable is visible on the server side when I log process.env
...
NVM_IOJS_ORG_MIRROR: 'https://iojs.org/dist',
GRAPHQL_URL: 'http://localhost:8080/graphql',
npm_config_cache_lock_wait: '10000',
npm_config_production: '',
...
but not on the client:
This is my .env
GRAPHQL_URL=http://localhost:8080/graphql
Using [email protected]
I must be missing something else.
The define plugin does replacements. It replaces process.env.foo
with the value of foo
in the code. It won't modify the process
object in the client.
@ericf so how do I get process.env.foo
to appear on the client?
@purplecones add a console.log(process.env.foo) and you'll see that it should work. If you go and inspect the source code, you'll see that it will have been replaced with console.log("FOO") (or whatever the value of process.env.foo).
You can check for webpack.DefinePlugin docs and examples elsewhere if needed.
But personally so far I've been now using (in my project https://github.com/relatenow/relate) a JSON config file. Might switch to env vars of the need arises.
The issue with using define plugin is its setting the env variable at BUILD time and not RUN time.
This can break some workflows that use CI/CD to build units.
@andrewmclagan Right, and this violates 12-factor, for those who care about such things. Instead you can have an env.js that is referenced in a index.html script tag before the app script tag that loads variables onto, say, window.env. Just make sure this file finds its way to the right spot at runtime via your favorite method (including having your server serve it). Also, if you're minifying, you'll want to read your variables as string keys, e.g. myURL = window['env']['MY_URL']
.
refferring to the dotenv example
https://github.com/zeit/next.js/tree/v3-beta/examples/with-dotenv
I have a a use case where i have a .env.dev .env.stage .env.production however when I build my stage build i cannot stop the babel or next configuration from using the production vars. Has anybody else come across this? I am using babel-plugin-inline-dotenv
I intend to pass the env to the top level package.json script and then load the .env through the .babelrc file
package.json
"build": "ENV=stage next build .",
{
"presets": [
"next/babel",
],
"env": {
"development": {
"plugins": [["inline-dotenv", {
"path": ".env.dev"
}
]]
},
"production": {
"plugins": [["inline-dotenv", {
"path": ".env.production"
}
]]
},
"stage": {
"plugins": [["inline-dotenv", {
"path": ".env.stage"
}
]]
}
}
}
I have also attempted to use inside my next.config.js
if (process.env.NODE_ENV === 'stage') {
require('dotenv').config({path: '/.env.stage'});
}
as well as in my index.js
require('dotenv').config({path: '/.env.stage'});
no luck, any suggestions on supporting multiple environment variables?
appended after edit: It would appear the belrc is overwriting any next configuration. I am trying to remove babelrc file now and see if I can configure using only the next.config
Hi @neverfox, @andrewmclagan, and everyone else,
We found a solution to maintaining 12-factor app guidelines by proxying the API from the front-end to the backend via http-proxy-middleware.
Start by bringing out a custom server.js file. Then add a new endpoint to proxy the API:
const proxy = require('http-proxy-middleware');
const server = express();
server.use(
'/graphql',
proxy({
target: process.env.GRAPHQL_HOST,
changeOrigin: true
})
);
And now when you createNetworkInterface
in initApollo.js
, you do:
const networkInterface = createNetworkInterface({
uri: process.browser ? '/graphql' : 'http://localhost:3000/graphql';
});
Note that we check process.browser
because apollo-client SSR requires an absolute URI per the documentation.
We use http://localhost:3000/graphql
because we are assuming you are running your front-end server on that localhost
and port 3000
. If your configuration is different, make sure you change it.
Now when you change the GRAPHQL_HOST
env variable, and run your server.js/Docker/whatever, it will actually update.
This is all done without using shitty webpack plugins/hacks at the cost of having to proxy your GraphQL requests via your front-end server. For 99% of people this should not matter much performance wise.
Hope this helps!
@ericf i tried your webpack/dotenv suggestion and i cannot get it to work. we are also proxying other env vars similar to what @huangbong is doing so the server-side part works fine. but this other variable is more a feature flag that we want to set that would affect how some views are presented. what else should i run after i add those lines to next.config.js
?
@Kielan i managed to make it work following the with-dotenv example. my concern now is if the env vars would be exposed in any way to the user if they know where to look. i just want one variable exposed. from what i see in the browser console, i cannot log the env vars (that is good) and the feature flag does apply (also good). i'm worried that some other file compiled somewhere exposes the rest of the variables. i'm new to nextjs and react in general so this may sound like a stupid question.
edit: it actually didn't work... must have been some remnant cache file… so i am back to square one :\
edit 2: ok so it works if i remove and reinstall the babel plugins, which seems odd. if i change the env var value alone and re-run (using yarn, yarn run dev
), the old value for the env var stays... if i remove the plugins altogether, yarn install, add plugins back, and yarn install, the value for the env var is refreshed. ¯\_(ツ)_/¯
i hope this wont be an issue in production
ok i think i solved by using the server-side aspect of dotenv
to protect sensitive keys and use that only for reverse-proxy-style cases in server.js
. for env vars that i need in the client side i use the babel inline plugins, using include
for only the env vars that i need exposed in the client side. i still have the reinstall issues as above but at least i'm making progress
The process.env
is always empty on the browser, I'm using [email protected]
I tried like about a 10 combinations of tries and nothing, also the with-dotenv
example only works in dev mode :(
help?
@sebas5384 I believe (if I'm not mistaken) webpack replaces references to process.env.FOO
with a literal string value, and doesn't do anything to produce a whole process.env
object. This is why "secret" environment variables (passwords, API keys, etc) are not exposed, as long as you don't reference them in your code. So it would be expected that process.env
is always empty on the browser. Hope that helps. Please let me know if I'm mistaken.
@zenflow make sense, but it seems like that's true only at build step, right? something like:
npm run build && GRAPHQL_HOST=http://graph.host npm run start
the variable is loaded at server, but not for client, and that's ok, but it should be a way to whitelist what variables could be exposed, not only at build but in run time too.
btw thanks for the help @zenflow 👍
@sebas5384 did you try with babel-plugin-inline-dotenv
and babel-plugin-transform-inline-environment-variables
in the with-dotenv example? i use that to expose an individual variable to the client in the .babelrc
file:
"env": {
"development": {
"plugins": [
["inline-dotenv", {
"include": [
"SITE_ENV"
]
}]
]
},
"production": {
"plugins": [
["transform-inline-environment-variables", {
"include": [
"SITE_ENV"
]
}]
]
}
},
then i use it in a component: {process.env.SITE_ENV === "foo" && <ShowThisComponentToFoo />}
as i mentioned, the value of the variable somehow remains even if i change it in the environment and i can only refresh it via uninstalling and reinstalling the plugins. but this is not an issue for me since the value will be constant in production
Most helpful comment
The issue with using define plugin is its setting the env variable at BUILD time and not RUN time.
This can break some workflows that use CI/CD to build units.