DEV Community

Cover image for Dynamic Links for Remix.Run
Franco Valdes
Franco Valdes

Posted on

Dynamic Links for Remix.Run

I want to open this with a disclaimer. I have no idea if this is best practice or even something we should be allowed to do but it worked for me and my use case and thought it was super cool.

Ok, now to the good part. In Remix, which I absolutely love so far, there are a few functions you can export to accomplish many things. LoaderFunction to do server side fetching prior to page load. ActionFunction to handle post request, mostly used for form submissions. MetaFunction to handle meta data and lastly a LinkFunction to add stylesheets, script tags, preload, etc. Read more about those here.

The problem I think exist is that the first three I mentioned, have access to the Remix context. So in these functions you have access to the node request, any params in the URL, etc. In the last one, the LinkFunction you do not. Now that does make sense to me. Maybe including that context slows down the overall page flow or maybe can cause a flicker of styles, not sure. I might start a discussion on Github to learn more about why the LinkFunction doesn't include the Remix context.

So what happens if you need to include a stylesheet based on a database query, or a cookie value? Lucky for you, this article tells you how.

In Remix, not all routes need to be react components and/or real pages on your website. You can just export a loader or action (or both) with no default export and tada you have an api endpoint. So lets try this out in conjunction to my link function dilemma...

In your root.tsx file, you export a function called links, this allows you to include links globally across your entire application. In my use case, I have a theme stored in my database that is user generated and need to load those styles in as well.

// root.tsx
export function links() {
  return [
    { rel: "stylesheet", href: styles },
    { rel: "stylesheet", href: "/user-theme.css" },
Enter fullscreen mode Exit fullscreen mode

Create a new file called user-theme[.]css.tsx. The bracket syntax looks funky but that how you can escape the period. I have used this syntax for robots[.]txt.tsx and sitemap[.]xml.tsx files, works great.

export const loader: LoaderFunction = async ({ request }) => {
  const { data, error } = await supabase.from("themes").select("*").maybeSingle();

   * data.styles looks like this...
   * {
   *   "--primary-color": "#hexvalue",
   *   "--secondary-color": "#hexvalue",
   *   ...
   * }
  let cssString = "";
  if (data && data.styles) {
    const { styles } = data;
    const cssVars = Object.keys(styles).reduce((acc, key) => {
      return [
        `${key}: ${styles[key]};`
    }, []).join("\n");

    cssString = `:root{${cssVars}}`

  return new Response(cssString, {
    headers: {
      "Content-type": "text/css; charset=UTF-8",
Enter fullscreen mode Exit fullscreen mode

So it looks like a lot is happening there but its quite simple. I am querying the database for the information I need, then parsing the JSON into a css string. I am using css variables so I inject those into a :root declaration and ship that with the text/css content type.

That it! Now my dynamic css from the database query is visible on my browser, all server side. There is no client side request, no weird flicker of styles while it loads.

Let me know what you think of this approach! I know my use case was database queries, but you can also access the request cookie and return a dark mode stylesheet and really anything you want.

Top comments (1)

fvaldes33 profile image
Franco Valdes

Thanks for the reply! I am storing the values in a JSON format, so I cant just dump the raw data into css. It also need it to be global css, not for just one component, hence the :root selector. Lastly, doing it this way everything stays server side and avoids any flickering of default styles.

Like many things, there is no right or wrong. Just whatever works best for your use case.