Last week I made a ⚡️ talk at Remix Copenhagen meetup. I thought I make a short blog post about the content if I already gather the material for it.

Lost in translation movie

There is a mistake in this code. Can you find it? 🔍

import type { Note } from '@prisma/client';
import { useCatch, useLoaderData } from '@remix-run/react';
import type { LoaderFunction } from '@remix-run/server-runtime';
import { json } from '@remix-run/server-runtime';
import { prisma } from '~/db.server';

/*
export type Note = {
  id: string
  title: string
  body: string
  createdAt: Date
  updatedAt: Date
}
*/

type LoaderData = {
  notes: Note[];
};

export const loader: LoaderFunction = async () => {
  const notes = await prisma.note.findMany({});
  return json({ notes });
};

export default function Problem() {
  const { notes } = useLoaderData<LoaderData>();

  return (
    <>
      {notes.map((note) => (
        <div
          key={note.id}
          className="p flex flex-auto flex-col p-4 sm:p-6 lg:p-8"
        >
          <h2 className="text-2xl font-bold">{note.title}</h2>
          <ul className="text-lg">
            <li>
              Created at: {Intl.DateTimeFormat('en-US').format(note.createdAt)}
            </li>
            <li>
              Updated at: {Intl.DateTimeFormat('en-US').format(note.updatedAt)}
            </li>
            <li>Body: {note.body}</li>
          </ul>
        </div>
      ))}
    </>
  );
}

⚠️ Solution

The problem is with the following lines:

<li>
  Created at: {Intl.DateTimeFormat("en-US").format(note.createdAt)}
</li>
<li>
  Updated at: {Intl.DateTimeFormat("en-US").format(note.updatedAt)}
</li>

Here, note.createdAt and note.updatedAt will have the type string instead of Date.


Explanation 📜

At page navigation

The client gets the code-split bundle for that page, and at time same time, the loader for that route gets evaluated, which is a Fetch request to the server. The response body of that request will be encoded in JSON, so it has to be serializable. Date and some other types don’t have a native JSON representation.

┌────────────┐                       ┌─────────────┐
│     🖥️     ◄───code split bundle───┤     ☁️      │
│  browser   ◄───data from loader────┤   server    │
└────────────┘                       └─────────────┘

Navigation

At full reload

A full reload on the same route will do a full server render, and the data will be just embedded in the HTML. Loaders will be evaluated on the server side, but the data still has to be serializable there too.

┌────────────┐                       ┌─────────────┐
│     🖥️     ◄───full code bundle────┤     ☁️      │
│  browser   ◄────prefilled html─────┤   server    │
└────────────┘                       └─────────────┘

Full reload


OK types

  • ✅ string
  • ✅ number
  • ✅ boolean
  • ✅ null
  • ✅ Array
  • ✅ Object

Problematic types

  • ❌ Date
  • ❌ BigInt
  • ❌ Set
  • ❌ Map
  • ❌ RegExp
  • ❌ undefined
  • ❌ Error
  • ❌ NaN

At least we get a linter warning since remix 1.6.5

Linter warning


How to solve it - stupid solution

Convert the strings to Dates. For this type, it will work fine, as the Date’s constructor accepts strings:

<li>
  Created at: {Intl.DateTimeFormat("en-US").format(new Date(note.createdAt))}
</li>
<li>
  Updated at: {Intl.DateTimeFormat("en-US").format(new Date(note.updatedAt))}
</li>

However, there are types in which this method won’t work. We need a more generic solution for this.


How to solve it - the proper solution

New features since remix 1.6.5

“We enhanced the type signatures of loader and useLoaderData to make it possible to infer the data type from the return type of its related server function. To enable this feature, you will need to use the LoaderArgs type from your Remix runtime package instead of typing the function directly:”

import type { LoaderFunction } from '@remix-run/[runtime]';
// -->
import type { LoaderArgs } from '@remix-run/[runtime]';

export const loader: LoaderFunction = async (args) => {
  return json<LoaderData>(data);
};
// -->
export async function loader(args: LoaderArgs) {
  return json(data);
}

“Then you can infer the loader data by using typeof loader as the type variable in useLoaderData:

let data = useLoaderData() as LoaderData;
// -->
let data = useLoaderData<typeof loader>();

With this change, you no longer need to manually define a LoaderData type (huge time and typo saver!), and we serialize all values so that useLoaderData can’t return types that are impossible over the network, such as Date objects or functions.”

Reference: Remix 1.6.5 release note

remix-typedjson

Drop in replacement for useLoaderData/json calls –> it will automatically convert your non-serializable types back and forth.

const loaderData = useLoaderData<typeof loader>();
// -->
const loaderData = useTypedLoaderData<typeof loader>();

and

return json({...})
// -->
return typedjson({...})

Adding these two together:

import type { LoaderArgs } from '@remix-run/server-runtime';
import { typedjson, useTypedLoaderData } from 'remix-typedjson';
import { prisma } from '~/db.server';

export const loader = async (_: LoaderArgs) => {
  const notes = await prisma.note.findMany({});
  return typedjson({ notes });
};

export default function Problem() {
  const { notes } = useTypedLoaderData<typeof loader>();

  return (
    <>
      {notes.map((note) => (
        <div
          key={note.id}
          className="p flex flex-auto flex-col p-4 sm:p-6 lg:p-8"
        >
          <h2 className="text-2xl font-bold ">{note.title}</h2>
          <ul className="text-lg">
            <li>
              Created at: {Intl.DateTimeFormat('en-US').format(note.createdAt)}
            </li>
            <li>
              Updated at: {Intl.DateTimeFormat('en-US').format(note.updatedAt)}
            </li>
            <li>Body: {note.body}</li>
          </ul>
        </div>
      ))}
    </>
  );
}

This works now as we are sending a __meta__ object with the original JSON content, so on the client side the necessary data could be converted to the right type.

New response


What about other Remix API?

Actions and fetchers are affected as well, but remix-typedjson handles them.

const actionData = useTypedActionData<typeof action>();

and

const fetcher = useTypedFetcher<typeof action>();

Could it be solved with superjson?

Yes, of course.

import { deserialize, serialize } from "superjson";
...
export const loader: LoaderFunction = async () => {
  const notes = await prisma.note.findMany({});
  return json(serialize({ notes }));
};

export default function Problem() {
  const { notes } = deserialize(
    useLoaderData() as SuperJSONResult
  ) as LoaderData;
  ...
}

However, remix-typesjson’s API is much more comfortable to use, not to mention that it’s faster and more lightweight than superjson.


Link for the presentation and code examples.