diff --git a/.changeset/thick-snails-compete.md b/.changeset/thick-snails-compete.md new file mode 100644 index 0000000000..e16e5bc32e --- /dev/null +++ b/.changeset/thick-snails-compete.md @@ -0,0 +1,27 @@ +--- +"react-router": patch +--- + +Fix types for `UIMatch` to reflect that the `loaderData`/`data` properties may be `undefined` + +- When an `ErrorBoundary` is being rendered, not all active matches will have loader data available, since it may have been their `loader` that threw to trigger the boundary +- The `UIMatch.data` type was not correctly handing this and would always reflect the presence of data, leading to the unexpected runtime errors when an `ErrorBoundary` was rendered +- ⚠️ This may cause some type errors to show up in your code for unguarded `match.data` accesses - you should properly guard for `undefined` values in those scenarios. + +```tsx +// app/root.tsx +export function loader() { + someFunctionThatThrows(); // ❌ Throws an Error + return { title: "My Title" }; +} + +export function Layout({ children }: { children: React.ReactNode }) { + let matches = useMatches(); + let rootMatch = matches[0] as UIMatch>>; + // ^ rootMatch.data is incorrectly typed here, so TypeScript does not + // complain if you do the following which throws an error at runtime: + let { title } = rootMatch.data; // 💥 + + return ...; +} +``` diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index 9b6f0db42c..0ffdf25f16 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -923,14 +923,23 @@ export interface UIMatch { */ params: AgnosticRouteMatch["params"]; /** - * The return value from the matched route's loader or clientLoader + * The return value from the matched route's loader or clientLoader. This might + * be `undefined` if this route's `loader` (or a deeper route's `loader`) threw + * an error and we're currently displaying an `ErrorBoundary`. * * @deprecated Use `UIMatch.loaderData` instead */ - data: Data; - /** The return value from the matched route's loader or clientLoader */ - loaderData: Data; - /** The {@link https://reactrouter.com/start/framework/route-module#handle handle object} exported from the matched route module */ + data: Data | undefined; + /** + * The return value from the matched route's loader or clientLoader. This might + * be `undefined` if this route's `loader` (or a deeper route's `loader`) threw + * an error and we're currently displaying an `ErrorBoundary`. + */ + loaderData: Data | undefined; + /** + * The {@link https://reactrouter.com/start/framework/route-module#handle handle object} + * exported from the matched route module + */ handle: Handle; }