diff --git a/.changeset/blue-cycles-check.md b/.changeset/blue-cycles-check.md
new file mode 100644
index 0000000000..c45b48f419
--- /dev/null
+++ b/.changeset/blue-cycles-check.md
@@ -0,0 +1,5 @@
+---
+"react-router-dom": patch
+---
+
+Properly serialize/deserialize ErrorResponse instances when using built-in hydration
diff --git a/package.json b/package.json
index 46a937dd23..b29785bf16 100644
--- a/package.json
+++ b/package.json
@@ -116,7 +116,7 @@
"none": "14.5 kB"
},
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
- "none": "10 kB"
+ "none": "10.5 kB"
},
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
"none": "16 kB"
diff --git a/packages/react-router-dom/__tests__/data-browser-router-test.tsx b/packages/react-router-dom/__tests__/data-browser-router-test.tsx
index a8b68ecb82..6375b79954 100644
--- a/packages/react-router-dom/__tests__/data-browser-router-test.tsx
+++ b/packages/react-router-dom/__tests__/data-browser-router-test.tsx
@@ -23,6 +23,7 @@ import {
Outlet,
createBrowserRouter,
createHashRouter,
+ isRouteErrorResponse,
useLoaderData,
useActionData,
useRouteError,
@@ -264,6 +265,40 @@ function testDomRouter(
`);
});
+ it("deserializes ErrorResponse instances from the window", async () => {
+ window.__staticRouterHydrationData = {
+ loaderData: {},
+ actionData: null,
+ errors: {
+ "0": {
+ status: 404,
+ statusText: "Not Found",
+ internal: false,
+ data: { not: "found" },
+ __type: "RouteErrorResponse",
+ },
+ },
+ };
+ let { container } = render(
+
+ Nope} errorElement={} />
+
+ );
+
+ function Boundary() {
+ let error = useRouteError();
+ return isRouteErrorResponse(error) ?
Yes!
: No :(
;
+ }
+
+ expect(getHtml(container)).toMatchInlineSnapshot(`
+ "
+
+ Yes!
+
+ "
+ `);
+ });
+
it("renders fallbackElement while first data fetch happens", async () => {
let fooDefer = createDeferred();
let { container } = render(
diff --git a/packages/react-router-dom/__tests__/data-static-router-test.tsx b/packages/react-router-dom/__tests__/data-static-router-test.tsx
index fcaf04aec3..b6bc31c2be 100644
--- a/packages/react-router-dom/__tests__/data-static-router-test.tsx
+++ b/packages/react-router-dom/__tests__/data-static-router-test.tsx
@@ -1,7 +1,10 @@
import * as React from "react";
import * as ReactDOMServer from "react-dom/server";
import type { StaticHandlerContext } from "@remix-run/router";
-import { unstable_createStaticHandler as createStaticHandler } from "@remix-run/router";
+import {
+ json,
+ unstable_createStaticHandler as createStaticHandler,
+} from "@remix-run/router";
import {
Outlet,
useLoaderData,
@@ -71,7 +74,7 @@ describe("A ", () => {
let { query } = createStaticHandler(routes);
let context = (await query(
- new Request("http:/localhost/the/path?the=query#the-hash", {
+ new Request("http://localhost/the/path?the=query#the-hash", {
signal: new AbortController().signal,
})
)) as StaticHandlerContext;
@@ -179,7 +182,7 @@ describe("A ", () => {
let { query } = createStaticHandler(routes);
let context = (await query(
- new Request("http:/localhost/the/path", {
+ new Request("http://localhost/the/path", {
signal: new AbortController().signal,
})
)) as StaticHandlerContext;
@@ -209,6 +212,55 @@ describe("A ", () => {
);
});
+ it("serializes ErrorResponse instances", async () => {
+ let routes = [
+ {
+ path: "/",
+ loader: () => {
+ throw json(
+ { not: "found" },
+ { status: 404, statusText: "Not Found" }
+ );
+ },
+ },
+ ];
+ let { query } = createStaticHandler(routes);
+
+ let context = (await query(
+ new Request("http://localhost/", {
+ signal: new AbortController().signal,
+ })
+ )) as StaticHandlerContext;
+
+ let html = ReactDOMServer.renderToStaticMarkup(
+
+
+
+ );
+
+ let expectedJsonString = JSON.stringify(
+ JSON.stringify({
+ loaderData: {},
+ actionData: null,
+ errors: {
+ "0": {
+ status: 404,
+ statusText: "Not Found",
+ internal: false,
+ data: { not: "found" },
+ __type: "RouteErrorResponse",
+ },
+ },
+ })
+ );
+ expect(html).toMatch(
+ ``
+ );
+ });
+
it("supports a nonce prop", async () => {
let routes = [
{
@@ -225,7 +277,7 @@ describe("A ", () => {
let { query } = createStaticHandler(routes);
let context = (await query(
- new Request("http:/localhost/the/path", {
+ new Request("http://localhost/the/path", {
signal: new AbortController().signal,
})
)) as StaticHandlerContext;
@@ -275,7 +327,7 @@ describe("A ", () => {
let { query } = createStaticHandler(routes);
let context = (await query(
- new Request("http:/localhost/the/path", {
+ new Request("http://localhost/the/path", {
signal: new AbortController().signal,
})
)) as StaticHandlerContext;
@@ -305,7 +357,7 @@ describe("A ", () => {
let { query } = createStaticHandler(routes);
let context = (await query(
- new Request("http:/localhost/the/path?the=query#the-hash", {
+ new Request("http://localhost/the/path?the=query#the-hash", {
signal: new AbortController().signal,
})
)) as StaticHandlerContext;
@@ -351,7 +403,7 @@ describe("A ", () => {
];
let context = (await createStaticHandler(routes).query(
- new Request("http:/localhost/", {
+ new Request("http://localhost/", {
signal: new AbortController().signal,
})
)) as StaticHandlerContext;
@@ -385,7 +437,7 @@ describe("A ", () => {
];
let context = (await createStaticHandler(routes).query(
- new Request("http:/localhost/", {
+ new Request("http://localhost/", {
signal: new AbortController().signal,
})
)) as StaticHandlerContext;
diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx
index 6cd01dc86f..5afd942ae0 100644
--- a/packages/react-router-dom/index.tsx
+++ b/packages/react-router-dom/index.tsx
@@ -14,7 +14,6 @@ import {
createPath,
useHref,
useLocation,
- useMatch,
useMatches,
useNavigate,
useNavigation,
@@ -42,7 +41,7 @@ import {
createHashHistory,
invariant,
joinPaths,
- matchPath,
+ ErrorResponse,
} from "@remix-run/router";
import type {
@@ -205,7 +204,7 @@ export function createBrowserRouter(
return createRouter({
basename: opts?.basename,
history: createBrowserHistory({ window: opts?.window }),
- hydrationData: opts?.hydrationData || window?.__staticRouterHydrationData,
+ hydrationData: opts?.hydrationData || parseHydrationData(),
routes: enhanceManualRouteObjects(routes),
}).initialize();
}
@@ -221,10 +220,45 @@ export function createHashRouter(
return createRouter({
basename: opts?.basename,
history: createHashHistory({ window: opts?.window }),
- hydrationData: opts?.hydrationData || window?.__staticRouterHydrationData,
+ hydrationData: opts?.hydrationData || parseHydrationData(),
routes: enhanceManualRouteObjects(routes),
}).initialize();
}
+
+function parseHydrationData(): HydrationState | undefined {
+ let state = window?.__staticRouterHydrationData;
+ if (state && state.errors) {
+ state = {
+ ...state,
+ errors: deserializeErrors(state.errors),
+ };
+ }
+ return state;
+}
+
+function deserializeErrors(
+ errors: RemixRouter["state"]["errors"]
+): RemixRouter["state"]["errors"] {
+ if (!errors) return null;
+ let entries = Object.entries(errors);
+ let serialized: RemixRouter["state"]["errors"] = {};
+ for (let [key, val] of entries) {
+ // Hey you! If you change this, please change the corresponding logic in
+ // serializeErrors in react-router-dom/server.tsx :)
+ if (val && val.__type === "RouteErrorResponse") {
+ serialized[key] = new ErrorResponse(
+ val.status,
+ val.statusText,
+ val.data,
+ val.internal === true
+ );
+ } else {
+ serialized[key] = val;
+ }
+ }
+ return serialized;
+}
+
//#endregion
////////////////////////////////////////////////////////////////////////////////
diff --git a/packages/react-router-dom/server.tsx b/packages/react-router-dom/server.tsx
index e668894779..6177f0ac8e 100644
--- a/packages/react-router-dom/server.tsx
+++ b/packages/react-router-dom/server.tsx
@@ -9,6 +9,7 @@ import {
IDLE_NAVIGATION,
Action,
invariant,
+ isRouteErrorResponse,
UNSAFE_convertRoutesToDataRoutes as convertRoutesToDataRoutes,
} from "@remix-run/router";
import type { Location, RouteObject, To } from "react-router-dom";
@@ -100,7 +101,7 @@ export function unstable_StaticRouterProvider({
let data = {
loaderData: context.loaderData,
actionData: context.actionData,
- errors: context.errors,
+ errors: serializeErrors(context.errors),
};
// Use JSON.parse here instead of embedding a raw JS object here to speed
// up parsing on the client. Dual-stringify is needed to ensure all quotes
@@ -139,6 +140,24 @@ export function unstable_StaticRouterProvider({
);
}
+function serializeErrors(
+ errors: StaticHandlerContext["errors"]
+): StaticHandlerContext["errors"] {
+ if (!errors) return null;
+ let entries = Object.entries(errors);
+ let serialized: StaticHandlerContext["errors"] = {};
+ for (let [key, val] of entries) {
+ // Hey you! If you change this, please change the corresponding logic in
+ // deserializeErrors in react-router-dom/index.tsx :)
+ if (isRouteErrorResponse(val)) {
+ serialized[key] = { ...val, __type: "RouteErrorResponse" };
+ } else {
+ serialized[key] = val;
+ }
+ }
+ return serialized;
+}
+
function getStatelessNavigator() {
return {
createHref(to: To) {