Skip to content
Hassam on Code
Go back

A Note to Myself on Frontend Environment Variables

Frontend Environment Variables

Reading process.env directly in components is risky for a few reasons:

Instead, validate the environment once with Zod and pass the result down through context.

Validate Once

// src/config.ts
import { z } from "zod";

const schema = z.object({
  REACT_APP_GRAPHQL_URL: z.string().url(),
  REACT_APP_ENABLE_CHAT: z.enum(["true", "false"]).default("false"),
});

export type Config = {
  graphqlUrl: string;
  chatEnabled: boolean;
};

export function parseConfig(env: Record<string, string | undefined>): Config {
  const parsed = schema.parse(env);
  return {
    graphqlUrl: parsed.REACT_APP_GRAPHQL_URL,
    chatEnabled: parsed.REACT_APP_ENABLE_CHAT === "true",
  };
}

A missing or invalid value throws an error that names the variable. The rest of the app gets real types, like a boolean for the chat flag.

Provide It With Context

// src/config-context.tsx
import { createContext, useContext } from "react";
import type { Config } from "./config";

export const ConfigContext = createContext<Config | null>(null);

export function useConfig() {
  const config = useContext(ConfigContext);
  if (!config) throw new Error("useConfig needs a ConfigContext provider");
  return config;
}
// src/index.tsx
const config = parseConfig({
  REACT_APP_GRAPHQL_URL: process.env.REACT_APP_GRAPHQL_URL,
  REACT_APP_ENABLE_CHAT: process.env.REACT_APP_ENABLE_CHAT,
});

createRoot(document.getElementById("root")!).render(
  <ConfigContext.Provider value={config}>
    <App />
  </ConfigContext.Provider>,
);

List each variable by name. The bundler only replaces direct references, so passing process.env as a whole won’t work in the browser.

Components then call useConfig():

const { chatEnabled } = useConfig();
return chatEnabled ? <ChatWidget /> : null;

In tests, wrap the component in a provider with whatever config you need.

To catch a missing variable before deploying, call parseConfig(process.env) from a small script in CI before the build.

Block New process.env Reads

Use the n/no-process-env rule from eslint-plugin-n, and turn it off only where the config is read:

// eslint.config.js
import n from "eslint-plugin-n";

export default [
  {
    plugins: { n },
    rules: { "n/no-process-env": "error" },
  },
  {
    files: ["src/index.tsx"],
    rules: { "n/no-process-env": "off" },
  },
];

Credit

Based on Massimiliano Mirra’s “The process.env frontend time bomb (plus: a sustainable definition of ‘fixed’)”.


Share this post:

Previous Post
Breadth-First Search (BFS) and Its Variants
Next Post
Renaming Local and Remote Git Branches