окно не определено при попытке доступа к переменным среды в Remix

Я пытаюсь получить некоторые переменные среды в браузере с помощью Remix, и я следил за этим:

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

Я точно выполнил шаги 1 и 2, однако не могу получить доступ из браузера. Я получаю эту ошибку: ReferenceError: window is not defined

И вот мой действительно простой компонент:

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

export default Test;

Если я закомментирую, я увижу в <body> вверху документа с window.ENV = {...}содержание. Однако раскомментировать console.log показывает мне сообщение об ошибке и нет <script>тег. Это говорит мне, что проблема связана с настройкой window.ENV из документации, а не из моего компонента.

Любые мысли будут оценены!

5 ответов

Вы не можете получить доступ к объекту в этом коде (рендеринг компонента), потому что он работает как на сервере (из-за рендеринга на стороне сервера), так и на клиенте (так же, как обычное приложение React на стороне клиента). А на сервере нет объекта или каких-то других браузерных API. Поэтому вам нужно написать этот код таким образом, чтобы он мог работать как на сервере, так и на клиенте.

Вы все еще можете использовать windowобъект позже, например, в useEffectили несколько onClickобработчик, потому что этот код будет работать только на стороне клиента:

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

// ...

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

Но иногда вам нужны эти значения env сразу, прямо в методе рендеринга. Что вы можете использовать loaderфункционировать в сочетании с useLoaderDataкрючок вот такой:

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

Альтернатива использованиюwindow.ENVШаблон, предложенный документацией Remix, заключается в создании корневого контекста React в вашем файле Remix route/root.tsx , который включает ваши переменные среды на стороне клиента в свойстве значения контекста. Затем получите доступ к переменным по мере необходимости через соответствующий хук без необходимости использованияuseEffectпервый. Ниже приведен упрощенный пример с TypeScript:

контекст/root-context.ts

      import { createContext, useContext } from 'react'

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

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

маршруты /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

Доступ через useRootContext

      import { useRootContext } from '~/context'

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

    console.log(stripePublicKey)
 
    return (<Outlet />)
}

Использование объекта глобального окна по-прежнему остается жизнеспособным подходом после документации Remix v1.15 по определению переменных среды браузера .

После добавления ENV в окно вам необходимо получить к нему доступ, что можно сделать, избегаяuseEffectили контекст , проверив, находитесь ли вы на стороне сервера или на стороне клиента в браузере, на основе ошибок в документации Remix.

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

Связанный

Я также приведу код TypeScript о том, как добавить свойство в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)
}

На уровне компонента в реакции нет способа получить доступ к dom, где виден объект окна, без использования вызова ловушки useEffect и последующей ссылки на окно изнутри.

Еще одна альтернативаwindowзаключается в использовании встроенногоuseMatchesперехватите и прочитайте оттуда данные корневого загрузчика, для чего не требуется отдельный контекст React. Этот узор был позаимствован из набора Grunge.

крючки/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;
}

крючки/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] || '';
}

компоненты/MyComponent.tsx

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


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

  return (<div>{myEnvVar}</div>);
}
Другие вопросы по тегам