Can't access process.env values using dynamic keys

I have a nextjs (not sure if it has any relevance or does it apply to nodejs as a whole) project in which I would like to access a value from process.env using a dynamic key:

const myKey = 'MY_KEY'
console.log(process.env[myKey]) //undefined

For reference, I have tried:

console.log(process.env['MY_KEY']) // gives value under MY_KEY
console.log(process.env.MY_KEY) // works too

Solution 1:

First of all, one thing to note, that if you want to use env variables on the client then you need to prefix them with NEXT_PUBLIC_, or use older way of exposing them to the client inside next.config.js.

As for the question, this is due to nature of how Webpack DefinePlugin works which I believe NextJs uses internally. It does direct text replacement at build time, in simpler words it just looks for process.env.MY_KEY string and replaces it with value. But if you destructure it then plugin just can't find process.env.MY_KEY string anymore and you are getting nothing (process.env object is generated anyway, but it will be empty).

Although this only true for client side code, because for server side Next actually uses real process.env object and destructuring will work there.

For example, if we have NEXT_PUBLIC_MY_KEY=somevalue and we log this somewhere in the code:

  const key = 'NEXT_PUBLIC_MY_KEY';

  console.log(process.env.NEXT_PUBLIC_MY_KEY);
  console.log(process.env['NEXT_PUBLIC_MY_KEY']);
  console.log(process.env[key]);
  console.log(process.env);

On client side you will get:

somevalue
somevalue
undefined
{} // empty object

And on server side you will get:

somevalue
somevalue
somevalue
{ ... } // object with all available env values, even system ones

There is a little bit of info about it in older env docs.

Workaround

You could probably use Runtime Configuration, but it has it's own limitations, for example, I think the page should be dynamic (should use getInitialProps or getServerSideProps) to work.

// next.config.js
module.exports = {
  publicRuntimeConfig: {
    myEnv: 'somevalue',
    // or actually use process.env variables, they are all available here
    myEnv2: process.env.MY_ENV
  },
};

import getConfig from 'next/config';

const key = 'myEnv';

getConfig().publicRuntimeConfig[key] // 'somevalue' 

Or just pass your variables through getServerSideProps like the other answer mentioned.

EDIT: I've actually just tested publicRuntimeConfig and it works even on static pages, at least if you are using next start. Not sure why docs says that the page should have getServerSideProps. So this might be the solution in the end.

Solution 2:

As someone else has already correctly mentioned, this problem occurs because that's how the Webpack DefinePlugin works.

Solution

Extending the webpack config. You can filter out all environment variables, starting with NEXT_PUBLIC_ and add them with the DefinePlugin webpack plugin to the process.env object. This way you can call them dynamically in the browser.

next.config.js

module.exports = {
  webpack: (config, { webpack, isServer }) => {
    const envs = {};
    
    Object.keys(process.env).forEach(env => {
      if (env.startsWith('NEXT_PUBLIC_')) {
        envs[env] = process.env[env];
      }
    })
    
    if (!isServer) {
      config.plugins.push(new webpack.DefinePlugin({
        'process.env': JSON.stringify(envs),
      }))
    }

    return config
  },
}

your page

import { useEffect } from 'react';

const keys = ['FOO', 'NEXT_PUBLIC_FOO'];

export default function Home() {
  useEffect(() => {
    keys.forEach(key => console.log(process.env[key]));
  }, []);

  return (
    <>
    </>
  )
}

Workaround

This workaround adds all environment variables prefixed with NEXT_PUBLIC to become dynamically available in the frontend.

import { useEffect } from 'react';

const keys = ['FOO', 'NEXT_PUBLIC_FOO'];

export default function Home(props) {
  useEffect(() => {
    keys.forEach(key => console.log(props.envs[key]));
  }, []);

  return (
    <>
    </>
  )
}

export async function getStaticProps() {
  const envs = {};
  
  Object.keys(process.env).forEach(env => {
    if (env.startsWith('NEXT_PUBLIC_')) {
      envs[env] = process.env[env];
    }
  })

  return {
    props: {
      envs,
    },
  }
}

The important part is, that you use the getStaticProps method. Here you filter out all environment variables starting with NEXT_PUBLIC_ and add them to the envs property. This allows them to be dynamically usable in the frontend.

In this example, the value of NEXT_PUBLIC_FOO will log to the console, while the value of FOO is undefined.