remix
remix copied to clipboard
Meta function can't access `loader` data when it throws
Context
Why is this important? Screen reader users often rely on title
's to infer the content of a page. And the loader
is used to dynamically set the title
property in meta
functions.
The inability to set title's dynamically on errors severely hinders this functionality.
What version of Remix are you using?
1.6.7
Steps to Reproduce
You need a loader
that throws.
import type { MetaFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import { Outlet, useCatch } from '@remix-run/react';
export const loader = () => {
if (Math.random() > 0.5) {
throw json(
{ message: 'not found', slug: 'foo', title: 'yolo' },
{ status: 400 },
);
}
return json({ title: 'Not throwing' });
};
export const meta: MetaFunction = parameters => {
console.log(parameters);
return {};
};
export default function MyComponent() {
return <p>Hello</>;
}
export function CatchBoundary() {
const caught = useCatch();
return (
<p>
{caught.status}: {caught.data.slug}
</p>
);
}
Expected Behavior
The meta
function should have access to the data of json
that was thrown
.
Actual Behavior
The meta
function doesn't see the loader
's data.
Possible Alternative
In case of errors you can set the title in the higher routes that didn't throw, but that would require all routes always getting all titles, which is bad for performance 😕
Or maybe there is another way to set the title
in CatchBoundary
s?
If you throw a response, it's not valid data from your loader, hence loader data is not present in your meta
function.
If you want to update the title from your <CatchBoundary/>
you can add a useEffect
export function CatchBoundary() {
const caught = useCatch()
useEffect(() => {
if (caught?.data?.title) {
document.title = caught.data.title
}
})
return (
<>
<h1>CatchBoundary</h1>
<p>Title should be {caught.data.title}</p>
<p>
{caught.status}: {caught.data.slug}
</p>
</>
)
}
https://codesandbox.io/s/remix-catchboundary-title-h2nske?file=/app/routes/index.tsx
@kiliman Thank you for this!
If you throw a response, it's not valid data from your loader, hence loader data is not present in your meta function.
Hmm, I don't get that. Why would we assume that? Wouldn't it make more sense to add some conditionals into the meta
function so it can handle happy as well as sad paths?
If you want to update the title from your <CatchBoundary/> you can add a useEffect
This definitely solves the issue, but seems like a hacky solution considering the Remix convention is to do it with meta
🤔
When you throw from a function, you don't get the same result as if the function returned data. The meta
function currently receives the returned loader data.
I'm not saying this can't be changed. Just explaining how it currently works today.
I guess my only concern is that if this was changed, would it be a breaking change? Since meta
doesn't expect thrown data, it may not be in the same shape as loader data, so existing meta
functions could break if they don't check. I think the only way it would work is if they simply added another property to the meta
arguments. Something like catchData
. Then existing code will work and new code can inspect that property as needed.
@ryanflorence I read in the decision doc for remix router/react-router that there may be no distinction between Error and Catch boundaries in the future. Have you considered adding the error and catch data to the meta
function? This way developers can have more control of the UI being generated in cases of non-happy paths.
@janhesters I ran across this issue as well, and settled on another admittedly hacky solution, albeit one that renders on the server-side. Here's a simplified version of my root component using a module-level closure to determine its meta title.
app/root.tsx
import type { MetaFunction } from "@remix-run/node";
import { Meta, Outlet, useCatch } from "@remix-run/react";
let metaTitle = "Initial title";
export const meta: MetaFunction = () => {
return {
title: metaTitle,
};
};
function Document(props: { children: React.ReactNode }) {
return (
<html>
<head>
<Meta />
</head>
<body>{props.children}</body>
</html>
);
}
export default function Root() {
return (
<Document>
<Outlet />
</Document>
);
}
export function CatchBoundary() {
const caught = useCatch();
metaTitle = `${caught.status}: ${caught.statusText}`
return (
<Document>
<p>{caught.data}</p>
</Document>
);
}
app/routes/index.tsx
import type { LoaderFunction } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
export const loader: LoaderFunction = ({ request }) => {
if (request.url.match(/[?|&]oops=/)) {
throw new Response("Oops!", { status: 400, statusText: "Bad request" });
}
return new Response("Success!");
};
export default function IndexRoute() {
const content = useLoaderData<string>();
return <p>{content}</p>;
}
Navigate to http:/localhost:3000/
and the initial title is rendered.
Navigate to http:/localhost:3000/?oops
and the title assigned within the CatchBoundary
is rendered.
On the face of it, passing error/catch data to a MetaFunction
would come in handy
@chaance is this handled by the new Meta API? 🙏🏼
@chaance is this handled by the new Meta API? 🙏🏼
No. We're aware of this limitation. I'll make sure something is on our roadmap if it isn't already.
🤖 Hello there,
We just published version v0.0.0-nightly-1d3d86e-20230817
which involves this issue. If you'd like to take it for a test run please try it out and let us know what you think!
Thanks!
🤖 Hello there,
We just published version 2.0.0-pre.0
which involves this issue. If you'd like to take it for a test run please try it out and let us know what you think!
Thanks!