Skip to content

Commit 6743013

Browse files
authored
Fix console warning for routes that do not export a default component (#7745)
1 parent 1f29147 commit 6743013

File tree

3 files changed

+155
-15
lines changed

3 files changed

+155
-15
lines changed

.changeset/empty-default-export.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@remix-run/react": patch
3+
---
4+
5+
Fix warning that could be logged when using route files with no `default` export
6+
- It seems our compiler compiles these files to export an empty object as the `default` which we can then end up passing to `React.createElement`, triggering the console warning, but generally no UI issues
7+
- By properly detecting these, we can correctly pass `Component: undefined` off to the React Router layer
8+
- This is technically an potential issue in the compiler but it's an easy patch in the `@remix-run/react` layer and hopefully disappears in a Vite world

packages/remix-react/__tests__/components-test.tsx

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import { createStaticHandler } from "@remix-run/router";
2+
import { act, fireEvent, render } from "@testing-library/react";
13
import * as React from "react";
2-
import { createMemoryRouter, RouterProvider } from "react-router-dom";
3-
import { fireEvent, render, act } from "@testing-library/react";
4+
import { createMemoryRouter, Outlet, RouterProvider } from "react-router-dom";
45

6+
import { RemixBrowser } from "../browser";
57
import type { LiveReload as ActualLiveReload } from "../components";
68
import { Link, NavLink, RemixContext } from "../components";
7-
9+
import invariant from "../invariant";
10+
import { RemixServer } from "../server";
811
import "@testing-library/jest-dom/extend-expect";
912

1013
// TODO: Every time we touch LiveReload (without changing the API) these tests
@@ -199,3 +202,126 @@ describe("<Link />", () => {
199202
describe("<NavLink />", () => {
200203
itPrefetchesPageLinks(NavLink);
201204
});
205+
206+
describe("<RemixServer>", () => {
207+
it("handles empty default export objects from the compiler", async () => {
208+
let staticHandlerContext = await createStaticHandler([{ path: "/" }]).query(
209+
new Request("http://localhost/")
210+
);
211+
invariant(
212+
!(staticHandlerContext instanceof Response),
213+
"Expected a context"
214+
);
215+
216+
let context = {
217+
manifest: {
218+
routes: {
219+
root: {
220+
hasLoader: false,
221+
hasAction: false,
222+
hasErrorBoundary: false,
223+
id: "root",
224+
module: "root.js",
225+
path: "/",
226+
},
227+
empty: {
228+
hasLoader: false,
229+
hasAction: false,
230+
hasErrorBoundary: false,
231+
id: "empty",
232+
module: "empty.js",
233+
index: true,
234+
parentId: "root",
235+
},
236+
},
237+
entry: { imports: [], module: "" },
238+
url: "",
239+
version: "",
240+
},
241+
routeModules: {
242+
root: {
243+
default: () => {
244+
return (
245+
<>
246+
<h1>Root</h1>
247+
<Outlet />
248+
</>
249+
);
250+
},
251+
},
252+
empty: { default: {} },
253+
},
254+
staticHandlerContext,
255+
future: {},
256+
};
257+
258+
jest.spyOn(console, "warn").mockImplementation(() => {});
259+
jest.spyOn(console, "error");
260+
261+
let { container } = render(
262+
<RemixServer context={context} url="http://localhost/" />
263+
);
264+
265+
expect(console.warn).toHaveBeenCalledWith(
266+
'Matched leaf route at location "/" does not have an element or Component. This means it will render an <Outlet /> with a null value by default resulting in an "empty" page.'
267+
);
268+
expect(console.error).not.toHaveBeenCalled();
269+
expect(container.innerHTML).toMatch("<h1>Root</h1>");
270+
});
271+
});
272+
273+
describe("<RemixBrowser>", () => {
274+
it("handles empty default export objects from the compiler", () => {
275+
window.__remixContext = {
276+
url: "/",
277+
state: {
278+
loaderData: {},
279+
},
280+
future: {},
281+
};
282+
window.__remixRouteModules = {
283+
root: {
284+
default: () => {
285+
return (
286+
<>
287+
<h1>Root</h1>
288+
<Outlet />
289+
</>
290+
);
291+
},
292+
},
293+
empty: { default: {} },
294+
};
295+
window.__remixManifest = {
296+
routes: {
297+
root: {
298+
hasLoader: false,
299+
hasAction: false,
300+
hasErrorBoundary: false,
301+
id: "root",
302+
module: "root.js",
303+
path: "/",
304+
},
305+
empty: {
306+
hasLoader: false,
307+
hasAction: false,
308+
hasErrorBoundary: false,
309+
id: "empty",
310+
module: "empty.js",
311+
index: true,
312+
parentId: "root",
313+
},
314+
},
315+
entry: { imports: [], module: "" },
316+
url: "",
317+
version: "",
318+
};
319+
320+
jest.spyOn(console, "error");
321+
322+
let { container } = render(<RemixBrowser />);
323+
324+
expect(console.error).not.toHaveBeenCalled();
325+
expect(container.innerHTML).toMatch("<h1>Root</h1>");
326+
});
327+
});

packages/remix-react/routes.tsx

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type {
66
} from "react-router-dom";
77
import { redirect, useRouteError } from "react-router-dom";
88

9-
import type { RouteModules } from "./routeModules";
9+
import type { RouteModule, RouteModules } from "./routeModules";
1010
import { loadRouteModule } from "./routeModules";
1111
import {
1212
fetchData,
@@ -73,7 +73,7 @@ export function createServerRoutes(
7373
let routeModule = routeModules[route.id];
7474
let dataRoute: DataRouteObject = {
7575
caseSensitive: route.caseSensitive,
76-
Component: routeModule.default,
76+
Component: getRouteModuleComponent(routeModule),
7777
ErrorBoundary: routeModule.ErrorBoundary
7878
? routeModule.ErrorBoundary
7979
: route.id === "root"
@@ -170,7 +170,7 @@ export function createClientRoutes(
170170
...(routeModule
171171
? // Use critical path modules directly
172172
{
173-
Component: routeModule.default,
173+
Component: getRouteModuleComponent(routeModule),
174174
ErrorBoundary: routeModule.ErrorBoundary
175175
? routeModule.ErrorBoundary
176176
: route.id === "root"
@@ -244,17 +244,9 @@ async function loadRouteModuleWithBlockingLinks(
244244
let routeModule = await loadRouteModule(route, routeModules);
245245
await prefetchStyleLinks(route, routeModule);
246246

247-
// Resource routes are built with an empty object as the default export -
248-
// ignore those when setting the Component
249-
let defaultExportIsEmptyObject =
250-
typeof routeModule.default === "object" &&
251-
Object.keys(routeModule.default || {}).length === 0;
252-
253247
// Include all `browserSafeRouteExports` fields
254248
return {
255-
...(routeModule.default != null && !defaultExportIsEmptyObject
256-
? { Component: routeModule.default }
257-
: {}),
249+
Component: getRouteModuleComponent(routeModule),
258250
ErrorBoundary: routeModule.ErrorBoundary,
259251
handle: routeModule.handle,
260252
links: routeModule.links,
@@ -299,3 +291,17 @@ function getRedirect(response: Response): Response {
299291
}
300292
return redirect(url, { status, headers });
301293
}
294+
295+
// Our compiler generates the default export as `{}` when no default is provided,
296+
// which can lead us to trying to use that as a Component in RR and calling
297+
// createElement on it. Patching here as a quick fix and hoping it's no longer
298+
// an issue in Vite.
299+
function getRouteModuleComponent(routeModule: RouteModule) {
300+
if (routeModule.default == null) return undefined;
301+
let isEmptyObject =
302+
typeof routeModule.default === "object" &&
303+
Object.keys(routeModule.default).length === 0;
304+
if (!isEmptyObject) {
305+
return routeModule.default;
306+
}
307+
}

0 commit comments

Comments
 (0)