Updating mergeMeta
utility suggested in Remix docs for use in React Router V7
#12672
-
Whilst upgrading a Remix app to use React Router V7, the I've created an example repo here which demonstrates how the existing util throws TS errors; https://github.com/charlie-bud/rr-v7.x-meta-types-repro I've also tried updating // app/utils/meta.ts
export const mergeMeta =
<MetaFn extends MetaFunction<unknown, Record<string, unknown>>>(
leafMetaFn: MetaFn,
) =>
(arg: MetaArgs) => {
const leafMeta = leafMetaFn(arg);
return arg.matches.reduceRight((acc, match) => {
// eslint-disable-next-line no-restricted-syntax
for (const parentMeta of match.meta) {
const index = acc?.findIndex(
(meta) =>
("name" in meta &&
"name" in parentMeta &&
meta.name === parentMeta.name) ||
("property" in meta &&
"property" in parentMeta &&
meta.property === parentMeta.property) ||
("title" in meta && "title" in parentMeta),
);
if (index === -1) {
// Parent meta not found in acc, so add it
acc?.push(parentMeta);
}
}
return acc;
}, leafMeta);
};
// app/routes/my-route.ts
export const meta = mergeMeta<Route.MetaFunction>(({ data }) => {
// Do something here
}) The above correctly infers the arguments passed to the callback (e.g.
I appreciate that typing has changed a fair bit in React Router V7, so rather than suggesting as an issue, I'm wondering if there's anyone who has had any luck in migrating |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 1 reply
-
I've developed a solution for my project by creating a new merge function called import type { ClientLoaderFunction, LoaderFunction, MetaDescriptor, MetaFunction } from 'react-router';
import type { CreateMetaArgs, MetaDescriptors } from 'react-router/route-module';
/**
* Merging helper
*
* {@link https://remix.run/docs/en/main/route/meta#meta-merging-helper}
*
* If you can't avoid the merge problem with global meta or index routes, we've created
* a helper that you can put in your app that can override and append to parent meta easily.
*
* @example
* ```typescript
* import type { MetaFunction } from 'react-router';
*
* import { mergeMeta } from '~/utils/meta-utils';
*
* export const meta: MetaFunction<typeof loader> = mergeMeta(({ data }) => {
* return [
* { title: "My Leaf Route" },
* ];
* });
*
* // In a parent route:
* import type { MetaFunction } from 'react-router';
*
* export const meta: MetaFunction<typeof loader> = ({ data }) => {
* return [
* { title: "My Parent Route" },
* { name: 'description', content: "This is the parent route" },
* ];
* }
* ```
* The resulting meta will contain both `title: 'My Leaf Route'` and `description: 'This is the parent route'`.
*/
export function mergeMeta<Loader extends LoaderFunction | ClientLoaderFunction | unknown = unknown, ParentsLoaders extends Record<string, LoaderFunction | ClientLoaderFunction | unknown> = Record<string, unknown>>(
leafMetaFn: MetaFunction<Loader, ParentsLoaders>,
): MetaFunction<Loader, ParentsLoaders> {
return (args) => {
const leafMeta = leafMetaFn(args);
return args.matches.reduceRight((acc, match) => {
for (const parentMeta of match.meta) {
addUniqueMeta(acc, parentMeta);
}
return acc;
}, leafMeta);
};
}
/**
* Merging helper that works with Route Module Type Safety
*
* If you can't avoid the merge problem with global meta or index routes, we've created
* a helper that you can put in your app that can override and append to parent meta easily.
*
* @example
* ```typescript
* import type { Route } from './+types/leaf';
*
* import { mergeRouteModuleMeta } from '~/utils/meta-utils';
*
* export const meta: Route.MetaFunction = mergeRouteModuleMeta(({ data }) => {
* return [
* { title: "My Leaf Route" },
* ];
* });
*
* // In a parent route:
* import type { Route } from './+types/root';
*
* export const meta: Route.MetaFunction = ({ data }) => {
* return [
* { title: "My Parent Route" },
* { name: 'description', content: "This is the parent route" },
* ];
* }
* ```
* The resulting meta will contain both `title: 'My Leaf Route'` and `description: 'This is the parent route'`.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function mergeRouteModuleMeta<TMetaArgs extends CreateMetaArgs<any>>(leafMetaFn: (args: TMetaArgs) => MetaDescriptors): (args: TMetaArgs) => MetaDescriptors {
return (args) => {
const leafMeta = leafMetaFn(args);
return args.matches.reduceRight((acc, match) => {
for (const parentMeta of match?.meta ?? []) {
addUniqueMeta(acc, parentMeta);
}
return acc;
}, leafMeta);
};
}
function addUniqueMeta(acc: MetaDescriptor[] | undefined, parentMeta: MetaDescriptor) {
if (acc?.findIndex((meta) => isMetaEqual(meta, parentMeta)) === -1) {
acc.push(parentMeta);
}
}
function isMetaEqual(meta1: MetaDescriptor, meta2: MetaDescriptor): boolean {
// prettier-ignore
return ('name' in meta1 && 'name' in meta2 && meta1.name === meta2.name) ||
('property' in meta1 && 'property' in meta2 && meta1.property === meta2.property) ||
('title' in meta1 && 'title' in meta2);
} |
Beta Was this translation helpful? Give feedback.
-
I've found a solution by copying over the necessary types in order to merge the titles for my titles. Parent export const loader = () => {
return { title: 'parent' };
}; Child export const loader = () => {
return { title: 'child' };
};
// This will result in: "child · parent"
export const meta: Route.MetaFunction = (args) => [{ title: mergeTitles(args) }]; Meta utility function // These types are copied over from React Router's route module as RouteInfo is not exported
type Func = (...args: any[]) => unknown;
type RouteModule = {
meta?: Func;
links?: Func;
headers?: Func;
loader?: Func;
clientLoader?: Func;
action?: Func;
clientAction?: Func;
HydrateFallback?: unknown;
default?: unknown;
ErrorBoundary?: unknown;
[key: string]: unknown;
};
type RouteInfo = {
parents: RouteInfo[];
module: RouteModule;
id: unknown;
file: string;
path: string;
params: unknown;
loaderData: unknown;
actionData: unknown;
};
export const mergeTitles = <T extends RouteInfo>({ matches }: CreateMetaArgs<T>) => {
return Object.values(matches)
.reduce<string[]>((titles, match) => {
if (!isLoaderDataWithTitle(match?.data)) {
return titles;
}
return [match.data.title, ...titles];
}, [])
.join(' · ');
}; |
Beta Was this translation helpful? Give feedback.
I've developed a solution for my project by creating a new merge function called
mergeRouteModuleMeta
to ensure type safety with Route Module Safety. We're gradually refactoring our numerous routes to incorporate this Route Module Type Safety.