Skip to content

Commit a69177e

Browse files
committed
fix: properly serialize/deserialize ErrorResponse instances
1 parent 67f16e7 commit a69177e

File tree

4 files changed

+153
-13
lines changed

4 files changed

+153
-13
lines changed

packages/react-router-dom/__tests__/data-browser-router-test.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
Outlet,
2424
createBrowserRouter,
2525
createHashRouter,
26+
isRouteErrorResponse,
2627
useLoaderData,
2728
useActionData,
2829
useRouteError,
@@ -264,6 +265,40 @@ function testDomRouter(
264265
`);
265266
});
266267

268+
it("deserializes ErrorResponse instances from the window", async () => {
269+
window.__staticRouterHydrationData = {
270+
loaderData: {},
271+
actionData: null,
272+
errors: {
273+
"0": {
274+
status: 404,
275+
statusText: "Not Found",
276+
internal: false,
277+
data: { not: "found" },
278+
__type: "RouteErrorResponse",
279+
},
280+
},
281+
};
282+
let { container } = render(
283+
<TestDataRouter window={getWindow("/")}>
284+
<Route path="/" element={<h1>Nope</h1>} errorElement={<Boundary />} />
285+
</TestDataRouter>
286+
);
287+
288+
function Boundary() {
289+
let error = useRouteError();
290+
return isRouteErrorResponse(error) ? <h1>Yes!</h1> : <h2>No :(</h2>;
291+
}
292+
293+
expect(getHtml(container)).toMatchInlineSnapshot(`
294+
"<div>
295+
<h1>
296+
Yes!
297+
</h1>
298+
</div>"
299+
`);
300+
});
301+
267302
it("renders fallbackElement while first data fetch happens", async () => {
268303
let fooDefer = createDeferred();
269304
let { container } = render(

packages/react-router-dom/__tests__/data-static-router-test.tsx

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import * as React from "react";
22
import * as ReactDOMServer from "react-dom/server";
33
import type { StaticHandlerContext } from "@remix-run/router";
4-
import { unstable_createStaticHandler as createStaticHandler } from "@remix-run/router";
4+
import {
5+
json,
6+
unstable_createStaticHandler as createStaticHandler,
7+
} from "@remix-run/router";
58
import {
69
Outlet,
710
useLoaderData,
@@ -71,7 +74,7 @@ describe("A <DataStaticRouter>", () => {
7174
let { query } = createStaticHandler(routes);
7275

7376
let context = (await query(
74-
new Request("http:/localhost/the/path?the=query#the-hash", {
77+
new Request("http://localhost/the/path?the=query#the-hash", {
7578
signal: new AbortController().signal,
7679
})
7780
)) as StaticHandlerContext;
@@ -179,7 +182,7 @@ describe("A <DataStaticRouter>", () => {
179182
let { query } = createStaticHandler(routes);
180183

181184
let context = (await query(
182-
new Request("http:/localhost/the/path", {
185+
new Request("http://localhost/the/path", {
183186
signal: new AbortController().signal,
184187
})
185188
)) as StaticHandlerContext;
@@ -209,6 +212,55 @@ describe("A <DataStaticRouter>", () => {
209212
);
210213
});
211214

215+
it("serializes ErrorResponse instances", async () => {
216+
let routes = [
217+
{
218+
path: "/",
219+
loader: () => {
220+
throw json(
221+
{ not: "found" },
222+
{ status: 404, statusText: "Not Found" }
223+
);
224+
},
225+
},
226+
];
227+
let { query } = createStaticHandler(routes);
228+
229+
let context = (await query(
230+
new Request("http://localhost/", {
231+
signal: new AbortController().signal,
232+
})
233+
)) as StaticHandlerContext;
234+
235+
let html = ReactDOMServer.renderToStaticMarkup(
236+
<React.StrictMode>
237+
<StaticRouterProvider
238+
router={createStaticRouter(routes, context)}
239+
context={context}
240+
/>
241+
</React.StrictMode>
242+
);
243+
244+
let expectedJsonString = JSON.stringify(
245+
JSON.stringify({
246+
loaderData: {},
247+
actionData: null,
248+
errors: {
249+
"0": {
250+
status: 404,
251+
statusText: "Not Found",
252+
internal: false,
253+
data: { not: "found" },
254+
__type: "RouteErrorResponse",
255+
},
256+
},
257+
})
258+
);
259+
expect(html).toMatch(
260+
`<script>window.__staticRouterHydrationData = JSON.parse(${expectedJsonString});</script>`
261+
);
262+
});
263+
212264
it("supports a nonce prop", async () => {
213265
let routes = [
214266
{
@@ -225,7 +277,7 @@ describe("A <DataStaticRouter>", () => {
225277
let { query } = createStaticHandler(routes);
226278

227279
let context = (await query(
228-
new Request("http:/localhost/the/path", {
280+
new Request("http://localhost/the/path", {
229281
signal: new AbortController().signal,
230282
})
231283
)) as StaticHandlerContext;
@@ -275,7 +327,7 @@ describe("A <DataStaticRouter>", () => {
275327
let { query } = createStaticHandler(routes);
276328

277329
let context = (await query(
278-
new Request("http:/localhost/the/path", {
330+
new Request("http://localhost/the/path", {
279331
signal: new AbortController().signal,
280332
})
281333
)) as StaticHandlerContext;
@@ -305,7 +357,7 @@ describe("A <DataStaticRouter>", () => {
305357
let { query } = createStaticHandler(routes);
306358

307359
let context = (await query(
308-
new Request("http:/localhost/the/path?the=query#the-hash", {
360+
new Request("http://localhost/the/path?the=query#the-hash", {
309361
signal: new AbortController().signal,
310362
})
311363
)) as StaticHandlerContext;
@@ -351,7 +403,7 @@ describe("A <DataStaticRouter>", () => {
351403
];
352404

353405
let context = (await createStaticHandler(routes).query(
354-
new Request("http:/localhost/", {
406+
new Request("http://localhost/", {
355407
signal: new AbortController().signal,
356408
})
357409
)) as StaticHandlerContext;
@@ -385,7 +437,7 @@ describe("A <DataStaticRouter>", () => {
385437
];
386438

387439
let context = (await createStaticHandler(routes).query(
388-
new Request("http:/localhost/", {
440+
new Request("http://localhost/", {
389441
signal: new AbortController().signal,
390442
})
391443
)) as StaticHandlerContext;

packages/react-router-dom/index.tsx

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
createPath,
1515
useHref,
1616
useLocation,
17-
useMatch,
1817
useMatches,
1918
useNavigate,
2019
useNavigation,
@@ -42,7 +41,7 @@ import {
4241
createHashHistory,
4342
invariant,
4443
joinPaths,
45-
matchPath,
44+
ErrorResponse,
4645
} from "@remix-run/router";
4746

4847
import type {
@@ -205,7 +204,7 @@ export function createBrowserRouter(
205204
return createRouter({
206205
basename: opts?.basename,
207206
history: createBrowserHistory({ window: opts?.window }),
208-
hydrationData: opts?.hydrationData || window?.__staticRouterHydrationData,
207+
hydrationData: opts?.hydrationData || parseHydrationData(),
209208
routes: enhanceManualRouteObjects(routes),
210209
}).initialize();
211210
}
@@ -221,10 +220,45 @@ export function createHashRouter(
221220
return createRouter({
222221
basename: opts?.basename,
223222
history: createHashHistory({ window: opts?.window }),
224-
hydrationData: opts?.hydrationData || window?.__staticRouterHydrationData,
223+
hydrationData: opts?.hydrationData || parseHydrationData(),
225224
routes: enhanceManualRouteObjects(routes),
226225
}).initialize();
227226
}
227+
228+
function parseHydrationData(): HydrationState | undefined {
229+
let state = window?.__staticRouterHydrationData;
230+
if (state && state.errors) {
231+
state = {
232+
...state,
233+
errors: deserializeErrors(state.errors),
234+
};
235+
}
236+
return state;
237+
}
238+
239+
function deserializeErrors(
240+
errors: RemixRouter["state"]["errors"]
241+
): RemixRouter["state"]["errors"] {
242+
if (!errors) return null;
243+
let entries = Object.entries(errors);
244+
let serialized: RemixRouter["state"]["errors"] = {};
245+
for (let [key, val] of entries) {
246+
// Hey you! If you change this, please change the corresponding logic in
247+
// serializeErrors in react-router-dom/server.tsx :)
248+
if (val && val.__type === "RouteErrorResponse") {
249+
serialized[key] = new ErrorResponse(
250+
val.status,
251+
val.statusText,
252+
val.data,
253+
val.internal === true
254+
);
255+
} else {
256+
serialized[key] = val;
257+
}
258+
}
259+
return serialized;
260+
}
261+
228262
//#endregion
229263

230264
////////////////////////////////////////////////////////////////////////////////

packages/react-router-dom/server.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
IDLE_NAVIGATION,
1010
Action,
1111
invariant,
12+
isRouteErrorResponse,
1213
UNSAFE_convertRoutesToDataRoutes as convertRoutesToDataRoutes,
1314
} from "@remix-run/router";
1415
import type { Location, RouteObject, To } from "react-router-dom";
@@ -100,7 +101,7 @@ export function unstable_StaticRouterProvider({
100101
let data = {
101102
loaderData: context.loaderData,
102103
actionData: context.actionData,
103-
errors: context.errors,
104+
errors: serializeErrors(context.errors),
104105
};
105106
// Use JSON.parse here instead of embedding a raw JS object here to speed
106107
// up parsing on the client. Dual-stringify is needed to ensure all quotes
@@ -139,6 +140,24 @@ export function unstable_StaticRouterProvider({
139140
);
140141
}
141142

143+
function serializeErrors(
144+
errors: StaticHandlerContext["errors"]
145+
): StaticHandlerContext["errors"] {
146+
if (!errors) return null;
147+
let entries = Object.entries(errors);
148+
let serialized: StaticHandlerContext["errors"] = {};
149+
for (let [key, val] of entries) {
150+
// Hey you! If you change this, please change the corresponding logic in
151+
// deserializeErrors in react-router-dom/index.tsx :)
152+
if (isRouteErrorResponse(val)) {
153+
serialized[key] = { ...val, __type: "RouteErrorResponse" };
154+
} else {
155+
serialized[key] = val;
156+
}
157+
}
158+
return serialized;
159+
}
160+
142161
function getStatelessNavigator() {
143162
return {
144163
createHref(to: To) {

0 commit comments

Comments
 (0)