Skip to content
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

Accessing environment variables in browser #159

Closed
sedubois opened this issue Oct 30, 2016 · 38 comments
Closed

Accessing environment variables in browser #159

sedubois opened this issue Oct 30, 2016 · 38 comments

Comments

@sedubois
Copy link
Contributor

I'm aware that secrets shouldn't be leaked in the browser (#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.

@nkzawa
Copy link
Contributor

nkzawa commented Oct 30, 2016

As far as I tested, it works fine.

// package.json
{
  "scripts": {
     "start": "MY_VAR=hi next",
  }
}
// pages/index.js
const env = 'undefined' !== process ? process.env.MY_VAR : null

export default class extends React.Component {
  static getInitialProps () {
    return { env }
  }
  render () {
    return <div>{this.props.env}</div>
  }
}

I did it like above.

MY_VAR=hi npm start also worked fine.

@sedubois
Copy link
Contributor Author

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: #106 (comment)

So this environment variable needs to be accessible on both client and server code.

@sedubois
Copy link
Contributor Author

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 sedubois changed the title Accessing environment variables Accessing environment variables through higher-order component Oct 31, 2016
@nkzawa
Copy link
Contributor

nkzawa commented Oct 31, 2016

@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.

@dstreet
Copy link
Contributor

dstreet commented Oct 31, 2016

@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} />;
    }
  };
}

@nkzawa
Copy link
Contributor

nkzawa commented Nov 3, 2016

@dstreet Your answer seems correct! Thanks.

Feel free to reopen if you still have the problem.

@amannn
Copy link
Contributor

amannn commented Nov 3, 2016

There's also https://github.com/mridgway/hoist-non-react-statics which is in general a good idea when creating HOCs.

@sedubois
Copy link
Contributor Author

sedubois commented Nov 4, 2016

@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

@sedubois
Copy link
Contributor Author

sedubois commented Nov 4, 2016

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...

@sedubois
Copy link
Contributor Author

sedubois commented Nov 4, 2016

Or maybe just wait for #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.

@nkzawa
Copy link
Contributor

nkzawa commented Nov 4, 2016

@sedubois you can embed env vars to dom like #159 (comment) .

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.

@nvartolomei
Copy link

nvartolomei commented Nov 4, 2016

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/',
            })
          }
        />
      );
    }
  };
}

@sedubois sedubois changed the title Accessing environment variables through higher-order component Accessing environment variables in browser Nov 4, 2016
@luisrudge
Copy link

this will only work in the first request, right? because only the first one is server rendered and has access to process.env

@nkzawa
Copy link
Contributor

nkzawa commented Nov 4, 2016

@luisrudge yep, so you have to embed env vars among all pages and enable to access these data when navigating on client.

@sedubois
Copy link
Contributor Author

sedubois commented Nov 4, 2016

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)

@nkzawa
Copy link
Contributor

nkzawa commented Nov 4, 2016

@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.

@sedubois
Copy link
Contributor Author

sedubois commented Nov 4, 2016

@nkzawa thanks, I'll just wait for the configurable Webpack for now.

@randallb
Copy link

randallb commented Dec 8, 2016

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.

@ericf
Copy link
Contributor

ericf commented Dec 21, 2016

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: #456)

@purplecones
Copy link

@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.

@ericf
Copy link
Contributor

ericf commented Dec 29, 2016

@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.

@jbaxleyiii
Copy link

@ericf I also can't seem to get this to work on beta.5. Identical webpack config to what you included

@ericf
Copy link
Contributor

ericf commented Jan 3, 2017

@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;
  },
};

@purplecones
Copy link

@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.

@ericf
Copy link
Contributor

ericf commented Jan 4, 2017

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.

@purplecones
Copy link

@ericf so how do I get process.env.foo to appear on the client?

@sedubois
Copy link
Contributor Author

@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.

@andrewmclagan
Copy link
Contributor

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.

@neverfox
Copy link

neverfox commented Jun 7, 2017

@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'].

@Kielan
Copy link

Kielan commented Jul 8, 2017

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

@mitchellhuang
Copy link

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!

@mgiraldo
Copy link

mgiraldo commented Oct 3, 2017

@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?

@mgiraldo
Copy link

mgiraldo commented Oct 3, 2017

@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

@mgiraldo
Copy link

mgiraldo commented Oct 4, 2017

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

@sebas5384
Copy link
Contributor

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?

@zenflow
Copy link
Contributor

zenflow commented Nov 1, 2017

@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.

@sebas5384
Copy link
Contributor

@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 👍

@mgiraldo
Copy link

mgiraldo commented Nov 2, 2017

@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

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests