How to render component only client side with Remix?
Asked Answered
F

6

7

I have a three.js component that should only be rendered client side, how do I do this with Remix? Does Remix expose any utilities to help determine if we're in a server or client context? Could not find anything.

Here is an example of what I'm attempting to do with fictional hooks that don't exist in remix.

import React from 'react';
import { useRemixContext } from '@remix-run/react';

const MyComponent = () => {
  const remixContext = useRemixContext();
  if(remixContext.server) {
     return null;
  }
  return <div>Should only be rendered on the client</div>
}
Fondle answered 11/3, 2023 at 16:45 Comment(1)
Check out <ClientOnly> from remix-utils github.com/sergiodxa/remix-utils#clientonlyOvertop
F
11

One can use the ClientOnly component exposed by the remix-utils library, which takes a function returning a component as a child which will be rendered client side only.

This component uses the hook useHydrated internally which is also exposed if you do not wish to use the component only for client side rendering.

Note that in development mode this did not originally work for me, I don't know if this is because I'm using remix-deno template or that it just does not work in general with the live reload, but once deployed in production it worked like a charm.

import React from 'react';
import { ClientOnly } from "remix-utils";

const App = () => {
  return <ClientOnly fallback={null}>
     {() => <MyComponent/>}
  </ClientOnly>
}

Also note that Suspense will eventually be released which should replace the above code.

Fondle answered 13/3, 2023 at 8:39 Comment(11)
Out of curiousity, are you still using remix-deno? If so, do you have this working in dev mode now with the latest tooling? TIALubricous
@T.J.Crowder the project I made is still running but I have not had the time to go back and update it. What is your experience with it?Fondle
Thanks. I don't have any, but I like Deno and was reading up on Remix for a new project and coincidentally saw this post, so I thought I'd ask. :-)Lubricous
My setup for deno-remix is probably long out of date, so many things have changed since then (I hope) because at the time compatibility between deno and node api + npm was rather complicated. I've since jumped on the bun hype train which has seamless integration with node api + npm.org. If you do try deno + remix and it's improved I would be highly interested in your feedback.Fondle
If I do, I'll let you know. I was considering Remix or Next.js for an early-stage project where so far I've just done a simple API layer and no hydration, etc. I still haven't decided if the pros outweigh the cons. I know Deno's drastically improved Node.js and npm compatibility in the last year, but there's still work to do.Lubricous
I can't say much about Next.js as I haven't worked with it extensively. I've worked with remix on early-stage projects as well and it gets the job done. The only issue I have are with the entry.client.tsx and the entry.server.tsx, whenever you want to do something a bit more complex, I find myself copying someone's vague config just to get it working, for example, when wanting to translate your app remix.run/resources/remix-i18next . When you start accumulating multiple of these unclear configs, those files become unmaintainable imo.Fondle
Ever the way with frameworks, you go a bit off the beaten path, and it gets hard quickly. :-) I have an issue with Next.js's hardcoded filenames like page.tsx and layout.tsx. Did we learn nothing from index.html in the 90s?! :-) The same name in 100 different directories is a nightmare. Which file am I being asked about here? Which layout is do I have open here? It's not enough to disqualify it, but it's definitely a black mark. I'm confused enough already, I don't need more confusion. ;-)Lubricous
@T.J.Crowder the solution is to not use routes folder (or in nextjs case apps or pages )to store the majority of your react components. they should only contain the thinnest of wrappers where the majority of your display and data layer components should be elsewhere with very obvious filenames.Unkempt
@Unkempt - I'd consider that a mitigation, not a solution. ;-)Lubricous
@T.J.Crowderoh forgot to mention that I recently started working with deno again and the fresh framework (supported by deno deploy) is really easy to pick up. It feels like a knockoff of remix. Worth a try... and yes another framework :DFondle
@T.J.Crowder tbh you should be doing that anyway. adding unreadable bloat to your routes definition section of your app is a great path to sphagettification. Not to mention painting yourself into a corner with regards to any possible testing or storybook work required in future.Unkempt
H
6

The remix-utils does not support the latest version of remix, which is 2.x, now. I can solve it by using React Suspense and lazy function.

import type { MetaFunction } from "@remix-run/cloudflare";
import React, { lazy, Suspense } from "react";
import "easymde/dist/easymde.min.css";

let SimpleMDE = lazy(async () => {
  const module = await import("react-simplemde-editor");

  return { default: module.default };
});
const MemoizedMDE = React.memo(SimpleMDE);

export default function Index() {
  return (
    <div>
      <h1>Simple MDE</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <MemoizedMDE />
      </Suspense>
    </div>
  );
}
Haycock answered 20/9, 2023 at 11:46 Comment(0)
A
0

Great solution from Yoshiki Masubuchi 👏. I have a slight variation on this, setting the Select in state. I was also struggling with remix-utils in Remix-v2 which I was using it as React-Select doesn't play well with Remix.

    import type { ComponentType } from 'react';
    import { useEffect, useState } from 'react';

    export const CurrencySelect = (): JSX.Element => {
      // react-select doesn't work on server https://github.com/JedWatson/react-select/issues/3590 so we import dynamically
      const [Select, setReactSelectImported] = useState < ComponentType < any >> ();

      useEffect(() => {
        const loadReactSelectOnClient = async() => {
          const module = await import ('react-select');
          setReactSelectImported(module.default);
        };

        loadReactSelectOnClient();
      }, []);

      return Select ? < Select menuPlacement = "auto"
      isSearchable = {
        false
      }
      /> : <div>Loading...</div > ;
    };
Arrowy answered 10/11, 2023 at 10:22 Comment(0)
D
0

Great solutions. I have one more solution using clientLoader and HydrateFallback. I like this solution because it is very declarative, but the drawback is that is only works for an entire route.

You can make your component hydrate only on the client by defining a clientLoader and instructing the component to only hydrate after the clientLoader ran with HydrateFallback.

Here's a proof-of-concept:

import { useState } from "react";

export async function clientLoader() {
  return null;
}

export function HydrateFallback() {
  return <h1>Loading...</h1>;
}

export default function test() {
  const [state, setState] = useState(localStorage.getItem("test"));
  return <h1>I'm a client-side component {state}</h1>;
}

Note that this will take a second to re-hydrate on the client, and will show the fallback on load.

Source: https://remix.run/docs/en/main/route/hydrate-fallback

Discant answered 6/2, 2024 at 12:31 Comment(2)
For this solution it would be better to use the clientLoader to load the data from localStorage and then having you read that information using useLoaderData. Since the whole point of the clientLoader / clientAction APIs is to mimic its server-side counterparts loader / action without the rendered components knowing or caring where the information comes from. Also on actions / revalidations you would want to have tapped in into Remix's "system" and get the most updated information from the loader / clientLoader.Solifluction
I think this only works for routes. not components imported from elsewhere in your monorepo.Unkempt
A
0

in most recent versions, rename the file to .client

Similar to what would happen with .server in the utils.

Some errors may arrise, such as "no string blalbalbla". Usually related to importing a server file from your client file.

https://remix.run/docs/en/main/discussion/server-vs-client

Aloud answered 13/9, 2024 at 5:31 Comment(0)
C
0

here is my simplest solution for REMIX:

//ClientOnly.tsx
import { useEffect, useState } from "react";


export default function ClientOnly({
  children
}: {
  children: React.ReactNode
}) {


  const [loaded, setLoaded] = useState(false)

  useEffect(() => {
    setLoaded(true)
  }, []);

  return (
    <>
      {
        loaded && (
          <>
            {children}
          </>
        )
      }
    </>
  )
}

And then:

<ClientOnly>

          <NavbarContent justify="end">
            <NavbarItem className="hidden lg:flex">
              <WalletButton></WalletButton>

            </NavbarItem>
          </NavbarContent>
        
      </ClientOnly>

Hope it will help someone )

Crinkle answered 25/9, 2024 at 16:40 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.