Cookie Consent by Free Privacy Policy Generator 📌 Runtime environmental variables in Next.js 14


✅ Runtime environmental variables in Next.js 14


💡 Newskategorie: Programmierung
🔗 Quelle: dev.to

This post will walk you through how to enable runtime environmental variables for both client and server components in a Dockerized Next.js app deployed on a custom-configured host.

Disclaimer: If you have a Dockerized Next.js application and you want to enable runtime environmental variables, then this post will help you; otherwise, if you use Vercel's cloud infrastructure for your deployments, Next.js documentation got you covered 😎.

Topics

  1. The Goal
  2. The Solution
    • Step 1: Configure Server Components and Environmental Variables
    • Step 2: Add script tag to main layout file
    • Step 3: Create React context
    • Step 4: Handle environmental variables in client components
  3. Testing
  4. Cons
  5. Conclusion

The Goal ⭐

One of the most important rules of a 12-factor application and key to continuous integration and development (CI/CD) is the Config rule. It specifies the separation of the code and the environmental variables, which makes an app agile to configuration changes and reduces its builds.

With the release of Next.js 14 and the unstable_noStore utility, we now have a new method of handling runtime environmental variables and implementing the "Build once, deploy many" strategy. In general, the goal of this strategy is to enable us to deploy a Docker image in multiple environments (e.g. production, staging, or development) by simply changing our configuration environmental variables based on the requirements of each environment, thus preventing the need to create a new image for each.

The graphic below shows how the aforementioned strategy will function with a Dockerized Next.js 14 application.

Build once, deploy many strategy

Static vs. runtime environmental variables

To get the following behavior in a Next.js app, we need to enable runtime environments. Our app uses two types of environmental variables:

  • runtime, which are evaluated during the application's runtime and should be changed dynamically each time it refreshes
  • static, which are only evaluated during build time and must be rebuilt to change their value.

So, in this post, we will go over how to handle runtime variables in our app's end-to-end flow.

Until the time of writing, there is no official example of how to enable runtime environmental variables in a Dockerized Next.js app, as utilizing unstable_noStore would only dynamically evaluate variables on the server (node.js runtime). There is also an interesting discussion regarding this topic on GitHub.

Essentially, the Next.js team proposes using an app router and eventually reading runtime environmental variables on each page.tsx route (server component) and passing those values down to the client or components using prop drilling.

❓ However, what if you have multiple deep-nested client components that require access to runtime environmental variables?

We need to figure out how to pass runtime environmental variables to the browser as they change dynamically on the server so that we can have a single source of truth for our environmental variables.

The Solution ✅

Now that we have a basic understanding of what we want to accomplish, we'll look at how to enable runtime environmental variables using the unstable_noStore function.

First and foremost, we can look at the figure below, which represents the solution that we will focus on.

Solution graphic example

The graphic above demonstrates how we intend to address our primary issue, which is dynamically altering environmental variables in the client's runtime.

If you check the visual example, I have highlighted the steps we will take to attain our goal.

1️⃣ The server component will dynamically read runtime environmental variables and insert them into a script tag.
2️⃣ We will append this script tag to the head of the HTML file, enabling it to run before the user interacts with the UI, eliminating loading states.
3️⃣ Using dom, a React context provider will retrieve runtime environmental variables from the script tag and hydrate the related client components.
4️⃣ To access runtime variables, client components will use the React context.

🔍 Let's now go over each step in more detail. 🔍

1️⃣: Configure Server Components and Environmental Variables

Using the example from the Next.js page, let's create a server component that reads the runtime environments. To keep our configuration simple for the tutorial, we will create a config.ts file with an object containing the values of the environmental variables. In addition, we will define a type for our configuration that reflects the types of each of our environmental variables, making variable management easier within the app. Finally, we will create the server component that will append the script tag to the DOM.

ℹ️ Note: It's worth noting here that we also use a nonce for our script tag, which is critical if you are using CSP rules for your application and want to avoid the browser blocking it.


// config.ts

type RuntimeEnvConfig = {
 my_value?: string;
 app_env_name?: string;
 flags: {
   enableFeatureA?: boolean;
 }
}

export const runtimeEnvConfig:  = {
  my_value: process.env.NEXT_PUBLIC_MY_VALUE,
  app_env_name: process.env.NEXT_PUBLIC_APP_ENV_NAME,
  flags: {
    enableFeatureA: process.env.NEXT_PUBLIC_ENABLE_FEATURE_A
  } 
}

// EnvVariablesScript.tsx

import { unstable_noStore as noStore } from 'next/cache';
import { runtimeEnvConfig } from './config.ts';

export default function EnvVariablesScript() {
  noStore();

  const nonce = headers().get('x-nonce');

  return <script id="env-config" nonce={nonce || ''}
      dangerouslySetInnerHTML={{
        __html: JSON.stringify(runtimeEnvConfig),
      }}  />;

}

2️⃣: Add script tag to main layout file

The component that renders the environmental variables script should be included in the app router's main layout.tsx files so that the script can be rendered to the domain as soon as possible. API keys or third-party integration urls, such as authentication configuration, may be environmental variables that client components should have access to before becoming interactive in order to avoid loading states. Therefore, the script tag should be included in the HTML document's <head /> section. This will include our script in the critical rendering path, requiring the browser to execute it before our web app becomes interactive for the users.

// app/layout.tsx

const RootLayout: FC<{ children: ReactNode }> = ({ children }) => {
  return (
    <html lang="en">
      <head>
        <EnvVariablesScript />
      </head>
      <body>
        {children}
      </body>
    </html>
  );
};

export default RootLayout;

3️⃣: Create React context

Steps 1️⃣ and 2️⃣ are about the server-side work that we need to do. Now that the server work is complete, we can move on to the client-side implementation.

In this stage, we'll develop a React context to keep the client components hydrated with runtime environmental variables.

'use client';

const defaultEnvVariables = {};

const EnvVariablesClientContext = createContext<EnvVariablesClientConfig>(defaultEnvVariables);

type EnvClientProviderProps = {
  children: ReactNode;
};

export const EnvVariablesClientProvider: React.FC<RuntimeEnvConfig> = ({ children }) => {
  const runtimeEnvVariables = {}; // temporary value

  return <EnvVariablesClientContext.Provider value={runtimeEnvVariables}>{children}</EnvVariablesClientContext.Provider>;
};

export const useEnvVariablesClientConfig = (): EnvVariablesClientConfig => {
  if (EnvVariablesClientContext === undefined) {
    throw new Error('useEnvVariablesClientConfig must be used within an EnvVariablesClientProvider');
  }

  return useContext(EnvVariablesClientContext);
};

Based on the code example above, we created a new context, a provider, and a hook for it, allowing us to simply and safely handle the context's value across our app. For the time being, our provider will persist an empty object via runtimeEnvVariables, as shown in the code.

4️⃣: Handle environmental variables in client components

Now, using DOM manipulation, we can make our context provider of greater use to our app. To retrieve runtime environmental variables directly from the provider's value, we must first establish an internal state that will populate the environmental variables during React's componentDidMount life cycle event.

First, let's define our value getter, which will retrieve the added environmental variables from the script tag.

'use client';

export const envScriptId = 'env-config';

const isSSR = typeof window === 'undefined';

export const getRuntimeEnv = (): EnvVariablesClientConfig => {
  if (isSSR) return env;
  const script = window.document.getElementById(envScriptId) as HTMLScriptElement;

  return script ? JSON.parse(script.innerText) : undefined;
};

ℹ️ Note: In the previous example, we used the isSSR check to prevent our getter from running during SSR since both the client and server components will be invoked and the DOM won't yet be ready.

Next, we'll define the internal state that will allow us to hydrate the children's components using the runtime environmental variables on mount.

export const EnvVariablesClientProvider: React.FC<EnvClientProviderProps> = ({ children }) => {
  const [envs, setEnvs] = useState<EnvVariablesClientConfig>(defaultEnvVariables);

  useEffect(() => {
    const runtimeEnvs = getRuntimeEnv();
    setEnvs(runtimeEnvs);
  }, []);

  return <EnvVariablesClientContext.Provider value={envs}>{children}</EnvVariablesClientContext.Provider>;
};

Finally, we will add the EnvVariablesClientProvider component to our app's main layout file.

// app/layout.tsx

const RootLayout: FC<{ children: ReactNode }> = ({ children }) => {
  return (
    <html lang="en">
      <head>
        <EnvVariablesScript />
      </head>
      <body>
        <EnvVariablesClientProvider>
          {children}
        </EnvVariablesClientProvider>
      </body>
    </html>
  );
};

export default RootLayout;

This way, we keep our main layout as a server component; as a result, the script tag containing our environmental variables will be generated on the server, eliminating the need for client components to wait for its rendering.

Using environmental variables in client components

We can now use runtime environmental variables on client components by invoking the useEnvVariablesClientConfig hook. As a result, we can now deploy feature A in the demo environment while hiding it in the production environment, eliminating the need for separate builds for each environment. We simply need to set the related environmental variable NEXT_PUBLIC_ENABLE_FEATURE_A=true in the environment's configuration.

const ClientComponent = () => {
  const config = useEnvVariablesClientConfig();

  const isFeatureAEnabled = config.flags?.enableFeatureA;

  return <>
           {isFeatureAEnabled && <p>feature A</p>}
           ...other features
        </>
}

Testing 🔬

Testing custom implementations, such as the current one, might get overwhelming. However, if we think of the test environment as being like a browser's DOM, we can use the same script tag that EnvVariableScript appends to the DOM during test init in the jest.setup.ts file.

import { runtimeEnvConfig } from '...';

beforeEach(() => {
  const envScript = document.createElement('script');
  envScript.setAttribute('id', envScriptId);
  envScript.innerText = JSON.stringify(runtimeEnvConfig);
  document.body.append(envScript);
});

afterEach(() => {
  document.getElementById(envScriptId)?.remove();
});

In some cases, we may want to override our environmental variables based on specific test conditions. For example, if we want to test the previous component to check if it displays the feature A UI, we can simply change the config.flags?.enableFeatureA value for each test case.

import merge from 'lodash/merge';

const overrideRuntimeEnv = (options: any, forceOverride = false) => {
  const script = document.getElementById(envScriptId);
  const existingData = script?.innerText ? JSON.parse(script?.innerText) : undefined;

  if (script) script.innerText = JSON.stringify(existingData && !forceOverride ? merge(existingData, options) : options);
};


it('should hide feature A when related flag is enabled', () => {
  overrideRuntimeEnv({ flags: { enableFeatureA: false } });

  const { container } = render(<ClientComponent />)

    expect(screen.queryByText('feature A')).not.toBeInTheDocument();

})

it('should hide feature A when related flag is missing', () => {
  overrideRuntimeEnv(undefined, true);

  const { container } = render(<ClientComponent />)

    expect(screen.queryByText('feature A')).not.toBeInTheDocument();
})


it('should display feature A when related flag is enabled', () => {
  overrideRuntimeEnv({ flags: { enableFeatureA: true } });
  const { container } = render(<ClientComponent />)

  expect(screen.getByText('feature A')).toBeInTheDocument();
})

Cons 👎

Although the above solution works well for using runtime environmental variables in client components in a dockerized Next.js app, Next.js does not officially support it yet, and using unstable_noStore will have some cons.

  1. It increases the complexity of the application. You should always keep in mind that you will need to maintain and use your own hook to retrieve environmental variable values.
  2. It has a performance cost for the application because the provider must hydrate components with the new configuration values during the 'componentDidMount' lifecycle event.
  3. It adds extra overhead to component testing because it introduces a new parameter to consider.

Conclusion 📋

This post demonstrated how to handle runtime environmental variables for a Dockerized Next.js application using a custom host. This solution, like almost every other in software engineering, has pros and cons, so please comment and provide feedback!
Thank you for reading the post. I hope this helped you solve your issue or come up with your own solution that best suits your needs.

PS: The open source package next-runtime-env provides an interesting and inspiring approach to the same problem.

...

✅ Runtime environmental variables in Next.js 14


📈 48.17 Punkte

✅ Apple highlights strides toward limiting environmental impact in annual Environmental Progress Report


📈 34.68 Punkte

✅ Automating Your Work with Makefile Incorporating Shell Commands and Environmental Variables


📈 31.39 Punkte

✅ Refactoring instance variables to local variables in Rails controllers


📈 28.1 Punkte

✅ Announcing runtime-environment: A Rust Crate for Detecting Operating Systems at Runtime


📈 22.05 Punkte

✅ Java SE Runtime Environment 8 8u391 - Java Runtime Environment (JRE).


📈 22.05 Punkte

✅ Runtime Broker High CPU Usage (FIXED): What Is Runtime Broker?


📈 22.05 Punkte

✅ Runtime Broker High CPU Usage (FIXED): What Is Runtime Broker?


📈 22.05 Punkte

✅ The birth of a new runtime – Alexander Larsson [Flatpak's Freedesktop runtime 18.08]


📈 22.05 Punkte

✅ Securing Next.js APIs with Middleware Using Environment Variables


📈 19.8 Punkte

✅ Twilio challenge - Environmental Bot


📈 17.34 Punkte

✅ CES's 'Worst in Show' Criticized Over Privacy, Security, and Environmental Threats


📈 17.34 Punkte

✅ Environmental and Foreign Affairs in the Context of AI: Human Security Beyond Trump


📈 17.34 Punkte

✅ Microsoft Cloud for Sustainability is aimed at environmental efforts


📈 17.34 Punkte

✅ Why Airpods Are An Environmental 'Tragedy'


📈 17.34 Punkte

✅ Apple TV+ lands Environmental Media Association Awards for ‘Jane’ and ‘Extrapolations’


📈 17.34 Punkte

✅ EPA Finalizes Rule Limiting Research Used for Public Health, Environmental Policy


📈 17.34 Punkte

✅ Samsung's Galaxy Note 7 Recall Is an Environmental Travesty


📈 17.34 Punkte

✅ How Must IT Leaders Develop Contingency Plans to Combat Geopolitical and Environmental Risks?


📈 17.34 Punkte

✅ Geostationary Operational Environmental Satellite-U (GOES-U) Launch


📈 17.34 Punkte

✅ Belkin renews environmental commitment with updated product line


📈 17.34 Punkte

✅ Free Returns Come With an Environmental Cost


📈 17.34 Punkte

✅ The real-time environmental impact of 250 000 ships | CON018


📈 17.34 Punkte

✅ How Microsoft is trying to reduce the environmental impact of its cloud


📈 17.34 Punkte

✅ Apple TV+ gewinnt Environmental Media Association Awards für „Jane“ und „Extrapolations“


📈 17.34 Punkte

✅ Environmental science degree jobs: All your options


📈 17.34 Punkte

✅ Oracle doubles down on environmental commitments


📈 17.34 Punkte

✅ On the road to 2030: Our 2022 Environmental Sustainability Report


📈 17.34 Punkte

✅ Download our environmental, social, and governance (ESG) reporting software enterprise buyer’s guide


📈 17.34 Punkte











matomo

Datei nicht gefunden!