Skip to content

Commit

Permalink
fix: properly serialize/deserialize ErrorResponse instances (#9593)
Browse files Browse the repository at this point in the history
* fix: properly serialize/deserialize ErrorResponse instances

* add changeset

* Bump bundle
  • Loading branch information
brophdawg11 committed Nov 23, 2022
1 parent f9652c6 commit 2cd8246
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/blue-cycles-check.md
@@ -0,0 +1,5 @@
---
"react-router-dom": patch
---

Properly serialize/deserialize ErrorResponse instances when using built-in hydration
35 changes: 35 additions & 0 deletions packages/react-router-dom/__tests__/data-browser-router-test.tsx
Expand Up @@ -23,6 +23,7 @@ import {
Outlet,
createBrowserRouter,
createHashRouter,
isRouteErrorResponse,
useLoaderData,
useActionData,
useRouteError,
Expand Down Expand Up @@ -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(
<TestDataRouter window={getWindow("/")}>
<Route path="/" element={<h1>Nope</h1>} errorElement={<Boundary />} />
</TestDataRouter>
);

function Boundary() {
let error = useRouteError();
return isRouteErrorResponse(error) ? <h1>Yes!</h1> : <h2>No :(</h2>;
}

expect(getHtml(container)).toMatchInlineSnapshot(`
"<div>
<h1>
Yes!
</h1>
</div>"
`);
});

it("renders fallbackElement while first data fetch happens", async () => {
let fooDefer = createDeferred();
let { container } = render(
Expand Down
66 changes: 59 additions & 7 deletions 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 {
Link,
Outlet,
Expand Down Expand Up @@ -239,7 +242,7 @@ describe("A <StaticRouterProvider>", () => {
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;
Expand Down Expand Up @@ -269,6 +272,55 @@ describe("A <StaticRouterProvider>", () => {
);
});

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(
<React.StrictMode>
<StaticRouterProvider
router={createStaticRouter(routes, context)}
context={context}
/>
</React.StrictMode>
);

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(
`<script>window.__staticRouterHydrationData = JSON.parse(${expectedJsonString});</script>`
);
});

it("supports a nonce prop", async () => {
let routes = [
{
Expand All @@ -285,7 +337,7 @@ describe("A <StaticRouterProvider>", () => {
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;
Expand Down Expand Up @@ -335,7 +387,7 @@ describe("A <StaticRouterProvider>", () => {
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;
Expand Down Expand Up @@ -365,7 +417,7 @@ describe("A <StaticRouterProvider>", () => {
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;
Expand Down Expand Up @@ -411,7 +463,7 @@ describe("A <StaticRouterProvider>", () => {
];

let context = (await createStaticHandler(routes).query(
new Request("http:/localhost/", {
new Request("http://localhost/", {
signal: new AbortController().signal,
})
)) as StaticHandlerContext;
Expand Down Expand Up @@ -445,7 +497,7 @@ describe("A <StaticRouterProvider>", () => {
];

let context = (await createStaticHandler(routes).query(
new Request("http:/localhost/", {
new Request("http://localhost/", {
signal: new AbortController().signal,
})
)) as StaticHandlerContext;
Expand Down
42 changes: 38 additions & 4 deletions packages/react-router-dom/index.tsx
Expand Up @@ -14,7 +14,6 @@ import {
createPath,
useHref,
useLocation,
useMatch,
useMatches,
useNavigate,
useNavigation,
Expand Down Expand Up @@ -42,7 +41,7 @@ import {
createHashHistory,
invariant,
joinPaths,
matchPath,
ErrorResponse,
} from "@remix-run/router";

import type {
Expand Down Expand Up @@ -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();
}
Expand All @@ -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

////////////////////////////////////////////////////////////////////////////////
Expand Down
21 changes: 20 additions & 1 deletion packages/react-router-dom/server.tsx
Expand Up @@ -10,6 +10,7 @@ import {
IDLE_NAVIGATION,
Action,
invariant,
isRouteErrorResponse,
UNSAFE_convertRoutesToDataRoutes as convertRoutesToDataRoutes,
} from "@remix-run/router";
import type { Location, RouteObject, To } from "react-router-dom";
Expand Down Expand Up @@ -99,7 +100,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
Expand Down Expand Up @@ -138,6 +139,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,
Expand Down

0 comments on commit 2cd8246

Please sign in to comment.