Why Environment Variables Matter in React Apps

Environment variables play a crucial role in frontend applications, allowing developers to configure settings without modifying the actual code. They are commonly used to store:

  • API base URLs – To connect with different backend services.
  • Feature flags – To enable or disable specific features dynamically.
  • Authentication keys – To manage secure access to APIs.
  • Third-party service configurations – For analytics, payment gateways, etc.

By using environment variables, we can keep sensitive information out of the codebase and easily manage different configurations for development, staging, and production environments.

The Common Issue: Rebuilding After Changing .env Variables

React applications (built with Create React App, Vite, or similar tools) inject environment variables at build time. This means:

  • If you modify an environment variable in the .env file, the changes won’t take effect until you rebuild the project using npm run build.
  • In production, changing something like an API URL or a feature flag requires a complete rebuild and redeployment.

This can be a problem in dynamic environments where configurations change frequently.

Why This is a Problem in Production

Imagine you’ve deployed a React app, and:

  • Your API URL changes based on the region where the user is located.
  • You want to toggle a feature flag without redeploying the entire frontend.
  • You’re using a CI/CD pipeline and need flexibility in setting environment variables at runtime.

Since React builds are static, you’re forced to rebuild the entire project just to update a configuration.

The Need for Runtime Environment Variables

Unlike backend applications (like Node.js), frontend React apps don’t naturally support runtime environment variables. This is because React is a static application once built, meaning the JavaScript files don’t change unless you rebuild them.

To solve this problem, we can use runtime environment variables, allowing us to update configurations without rebuilding the React app.

How Backend Frameworks Handle Environment Variables Differently

Backend frameworks (like Node.js) handle environment variables at runtime. Instead of being hardcoded during build time, they are accessed dynamically using process.env.

Example in a Node.js backend:

const express = require("express");
const app = express();

app.get("/api/config", (req, res) => {
  res.json({ apiUrl: process.env.API_URL });
});

app.listen(4000, () => {
  console.log("Server running on port 4000");
});

Here, we can change the API_URL environment variable without restarting or redeploying the application.

How to Inject Environment Variables at Runtime in a React App

Since React itself doesn’t support runtime environment variables, we can solve this by using a Node.js server to inject environment variables dynamically.

Step 1: Create a server.js File

We will use Express.js to serve the React app and dynamically generate a configuration file.

const express = require("express");
const path = require("path");
const dns = require("dns");

const app = express();

// Path to the React build folder
const staticDir = path.join(__dirname, "dist");
console.log("Serving static files from:", staticDir);
app.use(express.static(staticDir));

// Dynamic environment variable injection
app.get("/config.js", (req, res) => {
  dns.lookup(process.env.VITE_CORE_ENGINE_IP, (err, address) => {
    if (err) {
      console.error("DNS lookup failed:", err);
      res.status(500).send("window.ENV = { error: 'DNS lookup failed' }; ");
      return;
    }
    console.log("Resolved address:", address);
    res.setHeader("Content-Type", "application/javascript");
    res.send(`
      window._ENV_ = {
        VITE_CORE_ENGINE_IP: "${address}",
        VITE_CONFIG_PATH: "${process.env.VITE_CONFIG_PATH}"
      };
    `);
  });
});

// React Router fallback
app.get("*", (req, res) => {
  res.sendFile(path.join(staticDir, "index.html"));
});

// Start the server
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

How This Works:

  1. Serving Static Files:
    • express.static(staticDir) serves the built React files from the dist/ folder.
    • This makes the frontend accessible like a normal static website.
  2. Injecting Runtime Environment Variables:
    • The /config.js endpoint generates a JavaScript file dynamically.
    • It fetches values from process.env at runtime.
    • These values are injected into a window._ENV_ object that React can read.
  3. DNS Resolution for Dynamic IPs:
    • The dns.lookup() function resolves the backend service IP dynamically.
    • This is useful when running in Docker or Kubernetes, where backend services may have changing IPs.
  4. React Router Fallback:
    • The app.get("*") ensures React handles routing properly.
    • This prevents 404 errors when users navigate to different routes.

Step 2: Build the Docker Image

To package the application, create a Dockerfile:

FROM node:16
WORKDIR /app
COPY . .
RUN npm install && npm run build
CMD ["node", "server.js"]

Then, build the image:

docker build -t my-react-app .

Step 3: Run the Docker Container with Environment Variables

Now, run the container with dynamic environment variables:

docker run -p 4000:4000 -e VITE_CORE_ENGINE_IP=192.168.1.100 -e VITE_CONFIG_PATH=/new-path my-react-app

Benefits of Using Runtime Environment Variables

No Need to Rebuild the React App

  • The frontend dynamically fetches configuration values without a rebuild.
  • Updates to API URLs or feature flags take effect instantly.

Easier Deployment and CI/CD

  • The same frontend build works across staging, production, etc.
  • No need to maintain multiple builds for different environments.

Handles Dynamic Configurations

  • Useful for applications with auto-scaling, dynamic IPs, or third-party integrations.

Alternative: Using Nginx for Deployment

If you are using Nginx for deploying your React app, you can achieve the same effect without extra configuration. Instead of serving the app via a Node.js server, you can configure Nginx to serve static files and inject environment variables at runtime. In my case, I initially used the Node.js server approach, but later, we decided to go with Nginx, which simplified the deployment process.

Step 1: Build the Docker Image

To package the application, create a Dockerfile:

FROM node:16
WORKDIR /app
COPY . .
RUN npm install && npm run build
CMD ["node", "server.js"]

Then, build the image:

docker build -t my-react-app .

Step 2: Run the Docker Container with Environment Variables

Now, run the container with dynamic environment variables:

docker run -p 4000:4000 -e VITE_CORE_ENGINE_IP=192.168.1.100 -e VITE_CONFIG_PATH=/new-path my-react-app

Conclusion

Injecting runtime environment variables in a React app without rebuilding can save time and effort in production. Whether using a Node.js server or Nginx, this approach makes deployments more flexible and maintainable.

This approach is especially useful for CI/CD pipelines, microservices, and dynamic configurations, making it an excellent solution for modern frontend applications.

🚀 Now, you can change your configurations without worrying about rebuilding your entire app!

Author Of article : Rahul Sharma Read full article