# Use environment variables

Environment variables are incredibly useful when working with multiple environments, such as dev, staging, and production because they decouple configuration from code. This allows to change an application's behavior without modifying the code itself. A common example is the URLs of dedicated API services, where each environment uses a different URL.

By attaching environment variables to a FireflyRuntime instance, rather than accessing process.env throughout the codebase, Squide supports a modular architecture and makes it easier to write tests and Storybook stories by isolating configuration from global state.

While accessing environment variables through process.env works, it comes with several drawbacks:

  • Not ideal for testing: Mocking process.env for testing is cumbersome because it is a global variable. This often results in flaky tests, poor isolation, and unexpected side effects.
  • Not ideal for stories: Mocking process.env in Storybook stories can also be cumbersome.
  • Not ideal for modular code: Modules that rely on global variables are harder to load independently, reuse, or run in different host applications. This goes against modular design principles.
  • Couples the code to Node.js: Many environments do not support process.env, including browsers, Web Workers, Service Workers, Cloudflare Workers, Vercel Edge Functions, Netlify Edge Functions, and Deno (unless running in Node-compatibility mode).

For more details, refer to the reference documentation.

# Register variables at initialization

If some environment variables are already available at startup, they can be provided directly when initializing Squide:

import { initializeFirefly } from "@squide/firefly";

const runtime = initializeFirefly({
    environmentVariables: {
        apiBaseUrl: "https://my-api.com",
        telemetryBaseUrl: "https://my-telemetry.com"
    }
});

# Register a variable

import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerVariable("apiBaseUrl", "https://my-api.com");
};

# Register multiple variables at once

import type { ModuleRegisterFunction, FireflyRuntime } from "@squide/firefly";

export const register: ModuleRegisterFunction<FireflyRuntime> = runtime => {
    runtime.registerVariables({
        apiBaseUrl: "https://my-api.com",
        telemetryBaseUrl: "https://my-telemetry.com"
    });
};

# Retrieve a single variable

Using the useEnvironmentVariable hook:

import { useEnvironmentVariable } from "@squide/firefly";

const variable = useEnvironmentVariable("apiBaseUrl");

Using the runtime instance:

const variable = runtime.getEnvironmentVariable("apiBaseUrl");

# Retrieve all the variables

Using the useEnvironmentVariables hook:

import { useEnvironmentVariables } from "@squide/firefly";

const variables = useEnvironmentVariables();

Using the runtime instance:

const variables = runtime.environmentVariables;

# Setup the typings

Before registering variables, modules must augment the EnvironmentVariables interface with the variables they intend to register to ensure type safety and autocompletion.

First, create a types folder in the project:

project
├── src
├────── register.tsx
├────── Page.tsx
├────── index.tsx
├────── App.tsx
├── types
├────── env-vars.d.ts

Then create an env-vars.d.ts file:

project/types/env-vars.d.ts
import "@squide/firefly";

declare module "@squide/firefly" {
    interface EnvironmentVariables {
        // In the example above, the module only intends to register the `baseApiUrl` environment variable.
        baseApiUrl: string;
    }
}

Finally, update the project tsconfig.json to include the types folder:

project/tsconfig.json
{
    "compilerOptions": {
        "incremental": true,
        "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
        "types": [
            "./types/env-vars.d.ts"
        ]
    },
    "exclude": ["dist", "node_modules"]
}

If any other project using those environment variables must also reference the project's env-vars.d.ts file:

project/tsconfig.json
{
    "compilerOptions": {
        "incremental": true,
        "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
        "types": [
            "../another-project/types/env-vars.d.ts"
        ]
    },
    "exclude": ["dist", "node_modules"]
}

# Setup with tests

If the code under test uses environment variables, the FireflyRuntime instance can be used to mock these variables.

Considering the following utility hook:

./src/useAbsoluteUrl.ts
import { useEnvironmentVariable } from "@squide/firefly";

export function useAbsoluteUrl(path: string) {
    const apiBaseUrl = useEnvironmentVariable("apiBaseUrl");

    return `${apiBaseUrl}/${path}`;
}

The following unit test can be written to mock the value of apiBaseUrl and test the ouput of the useAbsoluteUrl hook:

./tests/useAbsoluteUrl.test.tsx
import { FireflyProvider, FireflyRuntime, EnvironmentVariablesPlugin } from "@squide/firefly";
import { renderHook } from "@testing-library/react";
import type { ReactNode } from "react";
import { useAbsoluteUrl } from "../src/useAbsoluteUrl.ts";

test("an absolute URL including the API base URL is returned", () => {
    const runtime = new FireflyRuntime({
        plugins: [x => new EnvironmentVariablesPlugin({
            variables: {
                apiBaseUrl: "https://my-api.com"
            }
        })]
    });

    const { result } = renderHook(() => useAbsoluteUrl("bar"), {
        wrapper: ({ children }: { children?: ReactNode }) => (
            <FireflyProvider runtime={runtime}>
                {children}
            </FireflyProvider>
        )
    })

    expect(result).toBe("https://my-api.com/bar");
});

# Setup with Storybook

To set up Storybook stories with environment variables, refer to the setup Storybook integration guide.