diff --git a/.changeset/hydrate-error-type.md b/.changeset/hydrate-error-type.md new file mode 100644 index 0000000000..674c77b2b9 --- /dev/null +++ b/.changeset/hydrate-error-type.md @@ -0,0 +1,5 @@ +--- +"react-router-dom": patch +--- + +Support proper hydration of `Error` subclasses such as `ReferenceError`/`TypeError` diff --git a/package.json b/package.json index 00636c56a0..29961053f5 100644 --- a/package.json +++ b/package.json @@ -118,10 +118,10 @@ "none": "16.2 kB" }, "packages/react-router-dom/dist/react-router-dom.production.min.js": { - "none": "12.6 kB" + "none": "12.7 kB" }, "packages/react-router-dom/dist/umd/react-router-dom.production.min.js": { - "none": "18.6 kB" + "none": "18.7 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 96d58d30d5..df6b13cf81 100644 --- a/packages/react-router-dom/__tests__/data-browser-router-test.tsx +++ b/packages/react-router-dom/__tests__/data-browser-router-test.tsx @@ -328,6 +328,49 @@ function testDomRouter( `); }); + it("deserializes Error subclass instances from the window", async () => { + window.__staticRouterHydrationData = { + loaderData: {}, + actionData: null, + errors: { + "0": { + message: "error message", + __type: "Error", + __subType: "ReferenceError", + }, + }, + }; + let router = createTestRouter( + createRoutesFromElements( + Nope} errorElement={} /> + ) + ); + let { container } = render(); + + function Boundary() { + let error = useRouteError() as Error; + return error instanceof Error ? ( + <> +
{error.toString()}
+
stack:{error.stack}
+ + ) : ( +

No :(

+ ); + } + + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+
+            ReferenceError: error message
+          
+
+            stack:
+          
+
" + `); + }); + it("renders fallbackElement while first data fetch happens", async () => { let fooDefer = createDeferred(); let router = createTestRouter( 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 546b4704ba..53a1c62737 100644 --- a/packages/react-router-dom/__tests__/data-static-router-test.tsx +++ b/packages/react-router-dom/__tests__/data-static-router-test.tsx @@ -701,6 +701,51 @@ describe("A ", () => { ); }); + it("serializes Error subclass instances", async () => { + let routes = [ + { + path: "/", + loader: () => { + throw new ReferenceError("oh no"); + }, + }, + ]; + let { query } = createStaticHandler(routes); + + let context = (await query( + new Request("http://localhost/", { + signal: new AbortController().signal, + }) + )) as StaticHandlerContext; + + let html = ReactDOMServer.renderToStaticMarkup( + + + + ); + + // stack is stripped by default from SSR errors + let expectedJsonString = JSON.stringify( + JSON.stringify({ + loaderData: {}, + actionData: null, + errors: { + "0": { + message: "oh no", + __type: "Error", + __subType: "ReferenceError", + }, + }, + }) + ); + expect(html).toMatch( + `` + ); + }); + it("supports a nonce prop", async () => { let routes = [ { diff --git a/packages/react-router-dom/index.tsx b/packages/react-router-dom/index.tsx index dc9edbeca8..4de210b97f 100644 --- a/packages/react-router-dom/index.tsx +++ b/packages/react-router-dom/index.tsx @@ -278,11 +278,30 @@ function deserializeErrors( val.internal === true ); } else if (val && val.__type === "Error") { - let error = new Error(val.message); - // Wipe away the client-side stack trace. Nothing to fill it in with - // because we don't serialize SSR stack traces for security reasons - error.stack = ""; - serialized[key] = error; + // Attempt to reconstruct the right type of Error (i.e., ReferenceError) + if (val.__subType) { + let ErrorConstructor = window[val.__subType]; + if (typeof ErrorConstructor === "function") { + try { + // @ts-expect-error + let error = new ErrorConstructor(val.message); + // Wipe away the client-side stack trace. Nothing to fill it in with + // because we don't serialize SSR stack traces for security reasons + error.stack = ""; + serialized[key] = error; + } catch (e) { + // no-op - fall through and create a normal Error + } + } + } + + if (serialized[key] == null) { + let error = new Error(val.message); + // Wipe away the client-side stack trace. Nothing to fill it in with + // because we don't serialize SSR stack traces for security reasons + error.stack = ""; + serialized[key] = error; + } } else { serialized[key] = val; } diff --git a/packages/react-router-dom/server.tsx b/packages/react-router-dom/server.tsx index 56bf96d48d..2271e51e48 100644 --- a/packages/react-router-dom/server.tsx +++ b/packages/react-router-dom/server.tsx @@ -179,6 +179,13 @@ function serializeErrors( serialized[key] = { message: val.message, __type: "Error", + // If this is a subclass (i.e., ReferenceError), send up the type so we + // can re-create the same type during hydration. + ...(val.name !== "Error" + ? { + __subType: val.name, + } + : {}), }; } else { serialized[key] = val;