react-router
react-router copied to clipboard
Fix types for UIMatch to reflect data may be undefined
A pattern that I see (and have used) a lot is to define methods on handle
that accept the current match
. Since this is invoked from a parent route, there's no guarantee that the data
for that route will be available (for example if the loader
errors).
For example, something like a breadcrumb
(pardon the lengthy setup):
// _layout.tsx
type MyRouteHandle<D = unknown> = {
breadcrumb: (match: UIMatch<D, MyRouteHandle<D>>, i: number, matches: UI) => ReactNode;
};
export default function Layout() {
const matches: UIMatch<MyRouteHandle>[] = useMatches();
const breadcrumbs: ReactNode[] = matches
.map((match, i, matches) => {
if (typeof match.handle?.breadcrumb === 'function') {
return match.handle.breadcrumb(match, i, matches);
}
return null;
})
.filter((n): n is ReactNode => !!n);
// render breadcrumbs
}
// _layout.some.sub.route.tsx
export async function loader({ request }: LoaderFunctionArgs) {
const user = await requireUser(request);
return json({ user });
}
export const handle: MyRouteHandle<typeof loader> {
breadcrumb: (match) => {
// Since `UIMatch<typeof loader>` resolves to a JS object, TS thinks that `match.data.user`
// is always defined, where in reality there is no guarantee that a particular match will actually have data (there are conditions where it is `undefined`).
// This will result in a "TypeError: cannot read properties of undefined"
const { username } = match.data.user;
return (
<li>
<Link to={`/users/${username}`}>{username}</Link>
</li>
);
}
};
By defining UIMatch['data']
to include undefined
, it directs the engineer to handle the case where there's no loader data, with additional help from TS when strict: true
.