Server vs. Client Code Execution

Server vs. Client Code Execution

Remix runs your app on the server as well as in the browser. However, it doesn't run all of your code in both places.

During the build step, the compiler creates both a server build and a client build. The server build bundles up everything into a single module (or multiple modules when using server bundles), but the client build splits your app up into multiple bundles to optimize loading in the browser. It also removes server code from the bundles.

The following route exports and the dependencies used within them are removed from the client build:

Consider this route module from the last section:

import type {
  ActionFunctionArgs,
  HeadersFunction,
  LoaderFunctionArgs,
} from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";

import { getUser, updateUser } from "../user";

export const headers: HeadersFunction = () => ({
  "Cache-Control": "max-age=300, s-maxage=3600",
});

export async function loader({
  request,
}: LoaderFunctionArgs) {
  const user = await getUser(request);
  return json({
    displayName: user.displayName,
    email: user.email,
  });
}

export default function Component() {
  const user = useLoaderData<typeof loader>();
  return (
    <Form action="/account">
      <h1>Settings for {user.displayName}</h1>

      <input
        name="displayName"
        defaultValue={user.displayName}
      />
      <input name="email" defaultValue={user.email} />

      <button type="submit">Save</button>
    </Form>
  );
}

export async function action({
  request,
}: ActionFunctionArgs) {
  const formData = await request.formData();
  const user = await getUser(request);

  await updateUser(user.id, {
    email: formData.get("email"),
    displayName: formData.get("displayName"),
  });

  return json({ ok: true });
}

The server build will contain the entire module in the final bundle. However, the client build will remove the action, headers and loader, along with the dependencies, resulting in this:

import { useLoaderData } from "@remix-run/react";

export default function Component() {
  const user = useLoaderData();
  return (
    <Form action="/account">
      <h1>Settings for {user.displayName}</h1>

      <input
        name="displayName"
        defaultValue={user.displayName}
      />
      <input name="email" defaultValue={user.email} />

      <button type="submit">Save</button>
    </Form>
  );
}

Splitting Up Client and Server Code

Out of the box, Vite doesn't support mixing server-only code with client-safe code in the same module. Remix is able to make an exception for routes because we know which exports are server-only and can remove them from the client.

There are a few ways to isolate server-only code in Remix. The simplest approach is to use .server and .client modules.

.server modules

While not strictly necessary, .server modules are a good way to explicitly mark entire modules as server-only. The build will fail if any code in a .server file or .server directory accidentally ends up in the client module graph.

app
ā”œā”€ā”€ .server šŸ‘ˆ marks all files in this directory as server-only
ā”‚   ā”œā”€ā”€ auth.ts
ā”‚   ā””ā”€ā”€ db.ts
ā”œā”€ā”€ cms.server.ts šŸ‘ˆ marks this file as server-only
ā”œā”€ā”€ root.tsx
ā””ā”€ā”€ routes
    ā””ā”€ā”€ _index.tsx

.server modules must be within your Remix app directory.

.server directories are only supported when using Remix Vite. The Classic Remix Compiler only supports .server files.

.client modules

You may depend on client libraries that are unsafe to even bundle on the server ā€” maybe it tries to access window by simply being imported.

You can remove the contents of these modules from the server build by appending *.client.ts to the file name or nesting them within a .client directory.

.client directories are only supported when using Remix Vite. The Classic Remix Compiler only supports .client files.

vite-env-only

If you want to mix server-only code and client-safe code in the same module, you can use vite-env-only. This Vite plugin allows you to explicitly mark any expression as server-only so that it gets replaced with undefined in the client.

For example, once you've added the plugin to your Vite config, you can wrap any server-only exports with serverOnly$:

import { serverOnly$ } from "vite-env-only";

import { db } from "~/.server/db";

export const getPosts = serverOnly$(async () => {
  return db.posts.findMany();
});

export const PostPreview = ({ title, description }) => {
  return (
    <article>
      <h2>{title}</h2>
      <p>{description}</p>
    </article>
  );
};

This example would be compiled into the following code for the client:

export const getPosts = undefined;

export const PostPreview = ({ title, description }) => {
  return (
    <article>
      <h2>{title}</h2>
      <p>{description}</p>
    </article>
  );
};
Docs and examples licensed under MIT