#accessing env vars on the client without passing them through a loader

1 messages · Page 1 of 1 (latest)

languid jewel
#

When exposing certain env vars to the client, most examples follow the advice here:
https://remix.run/docs/en/main/guides/envvars#browser-environment-variables

This works, but it's inefficient (in our case) to return the client env from the loader as it will be sent in every revalidation, and the data will never change (we can't return false in our root shouldRevalidate as we return other non-static data).

Is there any issue with simply setting the same global var on the server and client so everything works as expected on both server and client renders?

This example assumes that you have a custom server (ex. using the https://github.com/remix-run/react-router-templates/tree/main/node-custom-server template)

  1. add the global var to the server-side env in app.ts
globalThis["__ENV"] = {CDN_URL: "http://some.cdn.server"}
  1. add the global var to the window obj in root.tsx (before <Scripts/>)
<script dangerouslySetInnerHTML={{__html:`window["__ENV"] = ${JSON.stringify(__ENV)}`}}/>
  1. use it
<img src={`${__ENV.CDN_URL}/image.png`}/>
GitHub

Contribute to remix-run/react-router-templates development by creating an account on GitHub.

#

@forest flicker - with this method, I never access window on the server

#

to be more clear, I could access it with globalThis.__ENV

#

ex. <img src={${globalThis.__ENV.CDN_URL}/image.png}/>
works on both SSR and CSR

forest flicker
#

This way, I can use process.env server and client side

languid jewel
#

seems like that's pretty similar to what I'm doing - good to know 🙂

#

but I'm not sure why it needs to be returned from the loader if the data is truly static

forest flicker
#

because the UI doesn't have access

#

only the server have

#

and loaders run server-side

languid jewel
#

yeah, but I'm setting it on globalThis on the server in app.ts, and from there it's available on the SSR of root.tsx, which gives it to the client on the CSR

#

I'm gonna put together a very small POC, it'll probbaly be easier to see that way

forest flicker
#

globalThis server-side is not the same as globalThis client-side

#

they are completely different runtimes

#

if your component render <div>{globalThis.something}</div> then globalThis.something needs to be available in the server and browser

languid jewel
languid jewel
forest flicker
#

the issue is probably here

<script dangerouslySetInnerHTML={{__html:`window["__ENV"] = ${JSON.stringify(__ENV)}`}}/>
languid jewel
#

on SSR, it has access to __ENV, so it writes the script tag correctly, and when the client gets the SSR'd html, it sets __ENV on window

#

then during CSR, __ENV is there so everything works

#

to be clear, everything works as expected, I'm just wondering if there are any issues doing it this way, cause everything I've seen (including the documentation) says to send the client env via the loader

night berry
#

the dangerouslySetInnerHTML would get pinged by security scans, and rightfully so. But the way are using it I think is reasonably safe as the only variable we can rely on "json.stringify" to escape it properly, so low chance of xss there.

#

But, I do what sergio does and set config settings in the root loader. That context is available throughout the app. is there a reason you have an aversion to it?

#

you're just taking data and instead of using the context loading that's available to you, you're pushing it into a global scope.

forest flicker
#

also env variables can change at runtime, they are variables, not constants

#

in a long running process you could change the env variables without deploying again

languid jewel
night berry
#

"seems"

forest flicker
#

what dynamic data do you return from the root loader?

#

maybe you could move that to a layout route

#

and then add shouldRevalidate to the root route to prevent the loader to run again

#

so only the layout route loader will be revalidated

languid jewel
forest flicker
#

but if your solution works there's no reason to not do it

#

you could even prevent CSR of the <script> tag

#
<ServerOnly>
  render the script tag here
</ServerOnly>
languid jewel
night berry
#

This is the pattern we do. only like configuration and user prefs are in root loader. anything else "dynamic" (like flash messaging for example) are all on lower routes

forest flicker
#

then the script will be removed after hydration

#

another thing you can do is to inject a script into SSR in entry.server with bootstrapScripts in renderToPipeableStream

languid jewel
night berry
#

you could also have a resource route that generates a straight up js file and just load in a <script src="" tag.

forest flicker
#

if you're using remix-i18next I know recommend doing this

<html lang={i18n.language} dir={i18n.dir(i18n.language)}>

and i18n comes from let { i18n } = useTranslation();

languid jewel
#

those are interesting options, thanks - I'm not crazy about using global vars, I just wanted to make the POC as simple as possible

night berry
#

Oh I def load the lang in the root loader. that shouldn't change much for a user.

forest flicker
#

so you don't need to return the locale anymore from the root loader

languid jewel
forest flicker
#

and in entry.server you render

<I18nextProvider i18n={getI18nextInstance()}>
  <ServerRouter context={entryContext} url={request.url} />
</I18nextProvider>

this sets i18n with the already detected locale in I18nextProvider so your html tag will use the server locally on SSR

#

actually, you do need the locale in root, for useChangeLanguage hook to work

#

altough the hook receives the locale as an argument, so you could render it in another route

languid jewel
#

our original goal was to have root just return the lang and anything else that doesn't change often, although lang is all we have now, then have a layout route on top of everything else to handle our user auth (users can be optionally logged in on any page)

#

but then when you change the lang, it sends the client env down again, which I just don't like the idea of 🙂

#

our root shouldRevalidate only returned true when posting to /data/changelocale

languid jewel
#

I'll mess around with it more, as I prefer using RR features instead of lower level stuff so we have some flexibility with hosting

forest flicker
#

this doesn't depend on the hosting as entry.server file is part of your app

#

it's not the HTTP server

languid jewel
#

right, thanks - lots of levels to keep track of

#

especially coming from a SSR-only background

#

I'll give it another go now that I know locale doesn't necessarily have to be passed from the loader

#

probably something like this, assuming we can render useChangeLocale in the "base layout"

root (static stuff from env with shouldRevalidate=false)
+-- base layout (lang and other stuff that doesn't change often)
    +-- user layout (user auth, etc.)
        +-- all other routes
#

not sure how it'll work with error boundaries (i.e. changing a language on an error page, but that's an edge case we may not need to worry about)

south needle
#

You can prefix all env vars you want exposed with "VITE[inserthere]" and it will automatically be exposed to the client