window is not defined when trying to access environment variables in Remix

Viewed 2843

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!

3 Answers

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>
}

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 />)
}

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.

Related