7

I'm trying to get some environment variables into the browser with Remix and I've been following this:

https://remix.run/docs/en/v1/guides/envvars

I have followed steps 1 and 2 exactly, however I'm not able to access window.ENV from the browser. I'm getting this error: ReferenceError: window is not defined

And here is my really simple component:

function Test() {
  console.log('Window: ', window);
  return <div>Hello, Test</div>;
}

export default Test;

If I comment out the console.log I can see the <script> in the <body> towards the top of the document with the window.ENV = {...} contents. However uncommenting console.log shows me the error message and no <script> tag. This tells me the issue is with setting window.ENV from the documentation and not from my component.

Any thoughts would be appreciated!

themanatuf
  • 2,880
  • 2
  • 25
  • 39

5 Answers5

14

You can't access window object in that code (component render), because it runs both on the server (because of server side rendering) and on the client too (just as regular client side React app). And on the server there is no window object or any other browser APIs. So you need to write that code that way so it could run both on the server and the client.

You can still use window object later though, for example in useEffect or some onClick handler, because this code will only run on the client side:

// both this cases will work fine
  useEffect(() => {
    console.log(window.ENV);
  }, []);

// ...

      <button
        onClick={() => {
          console.log(window.ENV);
        }}
      >
        Log env
      </button>

But sometimes you need those env values right away, directly in the render method. What you can is to use loader function in combination with useLoaderData hook, like that:

export function loader() {
  // process.env is available here because loader runs only on the server side
  return {
    SOME_SECRET: process.env.SOME_SECRET
  };
}

export default function Index() {
  const data = useLoaderData();

  // here you can use everything that you returned from the loader function
  console.log(data.SOME_SECRET);

  return <div>{/* ... */}</div>
}
Danila
  • 15,606
  • 2
  • 35
  • 67
  • This jives with what I've been reading and learning about Remix. I have a much better understanding on how Remix works now, thanks! – themanatuf Jan 22 '22 at 14:14
  • 1
    I think we need a better way to handle this. it. seems awfully painful at the moment to handle something as simple as this. – CoderKK Feb 12 '22 at 15:47
8

An alternative to using the window.ENV pattern suggested by the Remix docs is to create a root React context in your Remix routes/root.tsx file that includes your client-side environment variables in the context's value prop. Then access the variables as needed via a corresponding useRootContext hook without the need to utilize useEffect first. Below is a simplified example with TypeScript:

context/root-context.ts

import { createContext, useContext } from 'react'

export const RootContext = createContext({
    stripePublicKey: '',
})

export const useRootContext = () => useContext(RootContext)

routes/root.tsx

import { useLoaderData, type LoaderFunction, json } from 'remix'
import { RootContext } from '~/context'

export const loader: LoaderFunction = async () => {
    return json({
        ENV: {
            stripePublicKey: process.env.STRIPE_PUBLIC_KEY,
        },
    })
}

const App = () => {
    const { ENV } = useLoaderData()

    return (
        <html lang="en">
            <body>
                <RootContext.Provider
                    value={{
                        stripePublicKey: ENV.stripePublicKey,
                    }}
                >
                    {/* app markup */}
                </RootContext.Provider>
            </body>
        </html>
    )
}

export default App

Access via useRootContext

import { useRootContext } from '~/context'

const BillingLayout = () => {
    const { stripePublicKey } = useRootContext()

    console.log(stripePublicKey)
 
    return (<Outlet />)
}
conceptlogic
  • 126
  • 1
  • 2
  • I appreciate this alternate method of providing variables from the server. Using the browser `window` object feels like a hack to me. Compared to that, this feels much more logical and mirrors established patterns in Remix (e.g. `useLoaderData()`). – Micah Yeager Apr 30 '23 at 18:44
1

At component level scope in react, there is no way to access the dom, where the window object is visible, without making using a useEffect hook call and then referencing window from inside there.

Oatrick
  • 21
  • 3
0

Using the global window object is still a viable approach following the Remix docs v1.15 on defining browser environment variables.

After adding ENV to window you need to access it which can be done by avoiding useEffect or context by checking if you are server-side or client-side in browser based on the Remix docs gotchas

if (!(typeof document === "undefined")) {
  console.log('browser environment so window is available', window.ENV)
}

Related

I'll also include the TypeScript code on how to add an property to window

interface ENV {
  NODE_ENV: typeof process.env.NODE_ENV;
}
declare global {
  interface Window {
    ENV: ENV;
  }
}

...

if (!(typeof document === "undefined")) {
  console.log('browser environment so window is available', window.ENV.NODE_ENV)
}
danactive
  • 799
  • 7
  • 14
0

Another alternative to window is to use the built in useMatches hook and read the root loader data from there, which doesn't require a separate React Context. This pattern was borrowed from the Grunge stack

hooks/useMatchesData.ts

export function useMatchesData(
  id: string,
): Record<string, unknown> | undefined {
  const matchingRoutes = useMatches();
  const route = useMemo(
    () => matchingRoutes.find((route) => route.id === id),
    [matchingRoutes, id],
  );
  return route?.data;
}

hooks/useEnv.ts

function isEnv(data: any): data is ENV {
  return data && typeof data === "object";
}

export function useEnvVar(name: keyof ENV) {
  const data = useMatchesData("root");
  if (!data || !isEnv(data.ENV)) {
    throw new Error("ENV not found");
  }
  return data.ENV[name] || '';
}

components/MyComponent.tsx

import { useEnvVar } from "~/hooks/useEnvVar";


export default function MyComponent() {
  const myEnvVar = useEnvVar('MY_ENV_VAR');

  return (<div>{myEnvVar}</div>);
}
Chris Feist
  • 1,678
  • 15
  • 17