Sergio Xalambrí

On-Demand Hydration in Remix

Remix makes it really easy to don't send JS to the browser. It even has a guide on how to let routes statically define if they need or not JS using the handle export, so some routes can be served without JS, and others will come with JS.

But this is an all or nothing. What if you decide if the route needs JS based on its data?

At my work, we use a service called Plaid. This service lets us connect to your bank account and do transfers securely. Imagine it's like Stripe for bank accounts instead of credit cards.

It works by giving you a React Hook. You pass a token, a callback, and receive a boolean to know if their SDK loaded and a method to open their own dialog UI. All of this needs JS to work.

let { open, ready } = usePlaidLink({ token, onSuccess });
return (
  <button type="button" onClick={open} disabled={!ready}>
    Connect Bank

But you don't always need to use this hook. If a user already linked their bank account, we could render something else. So we only need JS if the user hasn't connected their bank account yet.

Here's where it enters on-demand hydration. The handle export lets you export anything. We could export a function instead of a boolean and call it passing the loader's data.

Let's say we have a loader to check if the user has a bank account and get the token for Plaid.

type LoaderData = {
  plaidToken: string | null;

export let loader: LoaderFunction = async ({ request }) => {
  let token = await auth.isAuthenticated(request, { failureRedirect: "/" });
  let hasBankAccount = await checkBankAccount(token);
  // only get the plaid token if the user is authenticated and doesn't have a bank account already connected
  if (hasBankAccount) return json({ plaidToken: null });
  return json({ plaidToken: await getPlaidToken() });

We could make our handle export have a hydrate function like this:

export let handle = {
  hydrate(data: LoaderData) {
    return data.plaidToken !== null;

Now, we could use the useShouldHydrate hook from Remix Utils to detect if we need, or not, JS.

Or grab the code here

import { Links, LiveReload, Meta, Scripts, ScrollRestoration } from "remix";
import { useShouldHydrate } from "remix-utils";

export function Document({
}: {
  children: React.ReactNode;
  title?: string;
}) {
  let shouldHydrate = useShouldHydrate();
  return (
    <html lang="en" className="h-full">
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        {title ? <title>{title}</title> : null}
        <Meta />
        <Links />
      <body className="h-full">
        <ScrollRestoration />
        {/* Render Scripts if shouldHydrate is true */}
        {shouldHydrate && <Scripts />}
        {process.env.NODE_ENV === "development" && <LiveReload />}

That's it. Our route will only load the JS if the user really needs it and avoid it altogether if they don't, on-demand entirely based on its data.