react-router icon indicating copy to clipboard operation
react-router copied to clipboard

Fix types for UIMatch to reflect data may be undefined

Open rossipedia opened this issue 3 months ago • 3 comments

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.

rossipedia avatar Oct 29 '24 22:10 rossipedia