remix-run / remix

Build Better Websites. Create modern, resilient user experiences with web fundamentals.
https://remix.run
MIT License
29.19k stars 2.46k forks source link

MetaFunction data type not inferred from clientLoader #9238

Open cherewaty opened 5 months ago

cherewaty commented 5 months ago

Reproduction

https://stackblitz.com/edit/remix-run-remix-qggawd?file=app%2Froutes%2Fhello.tsx

System Info

System:
    OS: macOS 14.4.1
    CPU: (10) arm64 Apple M1 Pro
    Memory: 249.42 MB / 32.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.10.0 - ~/.nvm/versions/node/v20.10.0/bin/node
    Yarn: 4.1.1 - ~/.nvm/versions/node/v20.10.0/bin/yarn
    npm: 10.2.3 - ~/.nvm/versions/node/v20.10.0/bin/npm
  Browsers:
    Chrome: 122.0.6261.94
    Safari: 17.4.1

Used Package Manager

yarn

Expected Behavior

The docs for meta describe a TypeScript-friendly approach for using loader data in meta: https://remix.run/docs/en/main/route/meta#data

I have a Remix app in SPA mode using clientLoaders. I tried to take a similar approach to use typed clientLoader data in meta:

export async function clientLoader({ params }: ClientLoaderFunctionArgs) {
  return { task: { name: "Hello" } };
}

export const meta: MetaFunction<typeof clientLoader> = ({ data }) => {
  return [{ title: data.task.name }];
};

Actual Behavior

The code posted successfully passes the task with name Hello to meta, and it gets applied to the document title in the browser. But TypeScript reports that 'data' is of type 'unknown' inside of meta.

nightire commented 4 months ago

https://github.com/remix-run/remix/blob/98a012dee1a77c248d38d99b9768ee406751ec1b/packages/remix-server-runtime/routeModules.ts#L193

I believe it's because the generic only allows to extends from LoaderFunction.

RafalFilipek commented 4 months ago

Yeah, also it makes sense. You're rendering meta tags on the server, so in this case you should only rely on data provided by the loader.

nightire commented 4 months ago

Yeah, also it makes sense. You're rendering meta tags on the server, so in this case you should only rely on data provided by the loader.

This might not be the whole story since Remix also supports a SPA mode, I believe the meta function should work with clientLoader as well.

cherewaty commented 3 months ago

I've got a clunky workaround in place that gets me correct type information for data, but:

import { SerializeFrom } from "@remix-run/node";

export const meta = ({ data }: { data: SerializeFrom<typeof clientLoader> }) => {
  return [{ title: data.task.name }];
};
jucdev commented 2 months ago

I've got a clunky workaround in place that gets me correct type information for data, but:

  • doesn't add type protection for the return of meta, since MetaFunction doesn't match
  • Requires importing a type from @remix-run/node, which feels conceptually wrong for SPA mode
import { SerializeFrom } from "@remix-run/node";

export const meta = ({ data }: { data: SerializeFrom<typeof clientLoader> }) => {
  return [{ title: data.task.name }];
};

I created a new type until Remix fix that issue. When Remix will fix this issue, I'll only have to replace the occurrences of "ClientMetaFunction" by "MetaFunction" and update the imports.

import { SerializeFrom } from '@remix-run/node'
import { MetaDescriptor, MetaArgs } from '@remix-run/react'

type ClientMetaFunction<T> = (args: MetaArgs & { data: SerializeFrom<T> }) => MetaDescriptor[]

export const meta: ClientMetaFunction<typeof clientLoader> = ({ data }) => {
    return [{ title: `My page ${data.myParam}` }]
}

Let me know if I'm wrong.