Skip to content

Commit b7683f1

Browse files
fix inadvertent re-renders when using Component instead of element (#10287)
Co-authored-by: Mark Dalgleish <[email protected]>
1 parent 2a3c850 commit b7683f1

File tree

13 files changed

+162
-82
lines changed

13 files changed

+162
-82
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@remix-run/router": patch
3+
---
4+
5+
Deprecate the `createRouter` `detectErrorBoundary` option in favor of the new `mapRouteProperties` option for converting a framework-agnostic route to a framework-aware route. This allows us to set more than just the `hasErrorBoundary` property during route pre-processing, and is now used for mapping `Component -> element` and `ErrorBoundary -> errorElement` in `react-router`.

.changeset/fix-component-rerenders.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"react-router": patch
3+
"react-router-dom": patch
4+
---
5+
6+
Fix inadvertent re-renders when using `Component` instead of `element` on a route definition

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,13 @@
105105
},
106106
"filesize": {
107107
"packages/router/dist/router.umd.min.js": {
108-
"none": "44 kB"
108+
"none": "44.1 kB"
109109
},
110110
"packages/react-router/dist/react-router.production.min.js": {
111-
"none": "13 kB"
111+
"none": "13.1 kB"
112112
},
113113
"packages/react-router/dist/umd/react-router.production.min.js": {
114-
"none": "15.1 kB"
114+
"none": "15.3 kB"
115115
},
116116
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
117117
"none": "11.6 kB"

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -993,14 +993,15 @@ describe("A <StaticRouterProvider>", () => {
993993
expect(router.routes).toMatchInlineSnapshot(`
994994
[
995995
{
996-
"ErrorBoundary": [Function],
996+
"ErrorBoundary": undefined,
997997
"children": [
998998
{
999-
"ErrorBoundary": [Function],
999+
"ErrorBoundary": undefined,
10001000
"children": undefined,
10011001
"element": <h2>
10021002
Hi again!
10031003
</h2>,
1004+
"errorElement": <ErrorBoundary />,
10041005
"hasErrorBoundary": true,
10051006
"id": "0-0",
10061007
"path": "path",
@@ -1009,6 +1010,7 @@ describe("A <StaticRouterProvider>", () => {
10091010
"element": <h1>
10101011
Hi!
10111012
</h1>,
1013+
"errorElement": <ErrorBoundary />,
10121014
"hasErrorBoundary": true,
10131015
"id": "0",
10141016
"path": "the",
@@ -1022,14 +1024,15 @@ describe("A <StaticRouterProvider>", () => {
10221024
"pathname": "/the",
10231025
"pathnameBase": "/the",
10241026
"route": {
1025-
"ErrorBoundary": [Function],
1027+
"ErrorBoundary": undefined,
10261028
"children": [
10271029
{
1028-
"ErrorBoundary": [Function],
1030+
"ErrorBoundary": undefined,
10291031
"children": undefined,
10301032
"element": <h2>
10311033
Hi again!
10321034
</h2>,
1035+
"errorElement": <ErrorBoundary />,
10331036
"hasErrorBoundary": true,
10341037
"id": "0-0",
10351038
"path": "path",
@@ -1038,6 +1041,7 @@ describe("A <StaticRouterProvider>", () => {
10381041
"element": <h1>
10391042
Hi!
10401043
</h1>,
1044+
"errorElement": <ErrorBoundary />,
10411045
"hasErrorBoundary": true,
10421046
"id": "0",
10431047
"path": "the",
@@ -1048,11 +1052,12 @@ describe("A <StaticRouterProvider>", () => {
10481052
"pathname": "/the/path",
10491053
"pathnameBase": "/the/path",
10501054
"route": {
1051-
"ErrorBoundary": [Function],
1055+
"ErrorBoundary": undefined,
10521056
"children": undefined,
10531057
"element": <h2>
10541058
Hi again!
10551059
</h2>,
1060+
"errorElement": <ErrorBoundary />,
10561061
"hasErrorBoundary": true,
10571062
"id": "0-0",
10581063
"path": "path",

packages/react-router-dom/__tests__/exports-test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as ReactRouter from "react-router";
22
import * as ReactRouterDOM from "react-router-dom";
33

4-
let nonReExportedKeys = new Set(["UNSAFE_detectErrorBoundary"]);
4+
let nonReExportedKeys = new Set(["UNSAFE_mapRouteProperties"]);
55

66
describe("react-router-dom", () => {
77
for (let key in ReactRouter) {

packages/react-router-dom/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
UNSAFE_DataRouterStateContext as DataRouterStateContext,
2424
UNSAFE_NavigationContext as NavigationContext,
2525
UNSAFE_RouteContext as RouteContext,
26-
UNSAFE_detectErrorBoundary as detectErrorBoundary,
26+
UNSAFE_mapRouteProperties as mapRouteProperties,
2727
} from "react-router";
2828
import type {
2929
BrowserHistory,
@@ -220,7 +220,7 @@ export function createBrowserRouter(
220220
history: createBrowserHistory({ window: opts?.window }),
221221
hydrationData: opts?.hydrationData || parseHydrationData(),
222222
routes,
223-
detectErrorBoundary,
223+
mapRouteProperties,
224224
}).initialize();
225225
}
226226

@@ -234,7 +234,7 @@ export function createHashRouter(
234234
history: createHashHistory({ window: opts?.window }),
235235
hydrationData: opts?.hydrationData || parseHydrationData(),
236236
routes,
237-
detectErrorBoundary,
237+
mapRouteProperties,
238238
}).initialize();
239239
}
240240

packages/react-router-dom/server.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
createStaticHandler as routerCreateStaticHandler,
1818
UNSAFE_convertRoutesToDataRoutes as convertRoutesToDataRoutes,
1919
} from "@remix-run/router";
20+
import { UNSAFE_mapRouteProperties as mapRouteProperties } from "react-router";
2021
import type { Location, RouteObject, To } from "react-router-dom";
2122
import { Routes } from "react-router-dom";
2223
import {
@@ -206,12 +207,9 @@ function getStatelessNavigator() {
206207
};
207208
}
208209

209-
let detectErrorBoundary = (route: RouteObject) =>
210-
Boolean(route.ErrorBoundary) || Boolean(route.errorElement);
211-
212210
type CreateStaticHandlerOptions = Omit<
213211
RouterCreateStaticHandlerOptions,
214-
"detectErrorBoundary"
212+
"detectErrorBoundary" | "mapRouteProperties"
215213
>;
216214

217215
export function createStaticHandler(
@@ -220,7 +218,7 @@ export function createStaticHandler(
220218
) {
221219
return routerCreateStaticHandler(routes, {
222220
...opts,
223-
detectErrorBoundary,
221+
mapRouteProperties,
224222
});
225223
}
226224

@@ -231,7 +229,7 @@ export function createStaticRouter(
231229
let manifest: RouteManifest = {};
232230
let dataRoutes = convertRoutesToDataRoutes(
233231
routes,
234-
detectErrorBoundary,
232+
mapRouteProperties,
235233
undefined,
236234
manifest
237235
);

packages/react-router-native/__tests__/exports-test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as ReactRouter from "react-router";
22
import * as ReactRouterNative from "react-router-native";
33

4-
let nonReExportedKeys = new Set(["UNSAFE_detectErrorBoundary"]);
4+
let nonReExportedKeys = new Set(["UNSAFE_mapRouteProperties"]);
55

66
describe("react-router-native", () => {
77
for (let key in ReactRouter) {

packages/react-router/index.ts

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as React from "react";
12
import type {
23
ActionFunction,
34
ActionFunctionArgs,
@@ -207,27 +208,46 @@ export {
207208
useRoutes,
208209
};
209210

210-
function detectErrorBoundary(route: RouteObject) {
211-
if (__DEV__) {
212-
if (route.Component && route.element) {
213-
warning(
214-
false,
215-
"You should not include both `Component` and `element` on your route - " +
216-
"`element` will be ignored."
217-
);
211+
function mapRouteProperties(route: RouteObject) {
212+
let updates: Partial<RouteObject> & { hasErrorBoundary: boolean } = {
213+
// Note: this check also occurs in createRoutesFromChildren so update
214+
// there if you change this -- please and thank you!
215+
hasErrorBoundary: route.ErrorBoundary != null || route.errorElement != null,
216+
};
217+
218+
if (route.Component) {
219+
if (__DEV__) {
220+
if (route.element) {
221+
warning(
222+
false,
223+
"You should not include both `Component` and `element` on your route - " +
224+
"`Component` will be used."
225+
);
226+
}
218227
}
219-
if (route.ErrorBoundary && route.errorElement) {
220-
warning(
221-
false,
222-
"You should not include both `ErrorBoundary` and `errorElement` on your route - " +
223-
"`errorElement` will be ignored."
224-
);
228+
Object.assign(updates, {
229+
element: React.createElement(route.Component),
230+
Component: undefined,
231+
});
232+
}
233+
234+
if (route.ErrorBoundary) {
235+
if (__DEV__) {
236+
if (route.errorElement) {
237+
warning(
238+
false,
239+
"You should not include both `ErrorBoundary` and `errorElement` on your route - " +
240+
"`ErrorBoundary` will be used."
241+
);
242+
}
225243
}
244+
Object.assign(updates, {
245+
errorElement: React.createElement(route.ErrorBoundary),
246+
ErrorBoundary: undefined,
247+
});
226248
}
227249

228-
// Note: this check also occurs in createRoutesFromChildren so update
229-
// there if you change this
230-
return Boolean(route.ErrorBoundary) || Boolean(route.errorElement);
250+
return updates;
231251
}
232252

233253
export function createMemoryRouter(
@@ -249,7 +269,7 @@ export function createMemoryRouter(
249269
}),
250270
hydrationData: opts?.hydrationData,
251271
routes,
252-
detectErrorBoundary,
272+
mapRouteProperties,
253273
}).initialize();
254274
}
255275

@@ -273,5 +293,5 @@ export {
273293
RouteContext as UNSAFE_RouteContext,
274294
DataRouterContext as UNSAFE_DataRouterContext,
275295
DataRouterStateContext as UNSAFE_DataRouterStateContext,
276-
detectErrorBoundary as UNSAFE_detectErrorBoundary,
296+
mapRouteProperties as UNSAFE_mapRouteProperties,
277297
};

packages/react-router/lib/hooks.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,8 @@ function DefaultErrorComponent() {
495495
);
496496
}
497497

498+
const defaultErrorElement = <DefaultErrorComponent />;
499+
498500
type RenderErrorBoundaryProps = React.PropsWithChildren<{
499501
location: Location;
500502
error: any;
@@ -639,23 +641,17 @@ export function _renderMatches(
639641
// Only data routers handle errors
640642
let errorElement: React.ReactNode | null = null;
641643
if (dataRouterState) {
642-
if (match.route.ErrorBoundary) {
643-
errorElement = <match.route.ErrorBoundary />;
644-
} else if (match.route.errorElement) {
645-
errorElement = match.route.errorElement;
646-
} else {
647-
errorElement = <DefaultErrorComponent />;
648-
}
644+
errorElement = match.route.errorElement || defaultErrorElement;
649645
}
650646
let matches = parentMatches.concat(renderedMatches.slice(0, index + 1));
651647
let getChildren = () => {
652-
let children: React.ReactNode = outlet;
648+
let children: React.ReactNode;
653649
if (error) {
654650
children = errorElement;
655-
} else if (match.route.Component) {
656-
children = <match.route.Component />;
657651
} else if (match.route.element) {
658652
children = match.route.element;
653+
} else {
654+
children = outlet;
659655
}
660656
return (
661657
<RenderedRoute

packages/router/README.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
The `@remix-run/router` package is a framework-agnostic routing package (sometimes referred to as a browser-emulator) that serves as the heart of [React Router][react-router] and [Remix][remix] and provides all the core functionality for routing coupled with data loading and data mutations. It comes with built-in handling of errors, race-conditions, interruptions, cancellations, lazy-loading data, and much, much more.
44

5-
If you're using React Router, you should never `import` anything directly from the `@remix-run/router` or `react-router` packages, but you should have everything you need in either `react-router-dom` or `react-router-native`. Both of those packages re-export everything from `@remix-run/router` and `react-router`.
5+
If you're using React Router, you should never `import` anything directly from the `@remix-run/router` - you should have everything you need in `react-router-dom` (or `react-router`/`react-router-native` if you're not rendering in the browser). All of those packages should re-export everything you would otherwise need from `@remix-run/router`.
66

77
> **Warning**
88
>
@@ -16,11 +16,16 @@ A Router instance can be created using `createRouter`:
1616
// Create and initialize a router. "initialize" contains all side effects
1717
// including history listeners and kicking off the initial data fetch
1818
let router = createRouter({
19-
// Routes array
20-
routes: ,
21-
// History instance
22-
history,
23-
}).initialize()
19+
// Required properties
20+
routes, // Routes array
21+
history, // History instance
22+
23+
// Optional properties
24+
basename, // Base path
25+
mapRouteProperties, // Map function framework-agnostic routes to framework-aware routes
26+
future, // Future flags
27+
hydrationData, // Hydration data if using server-side-rendering
28+
}).initialize();
2429
```
2530

2631
Internally, the Router represents the state in an object of the following format, which is available through `router.state`. You can also register a subscriber of the signature `(state: RouterState) => void` to execute when the state updates via `router.subscribe()`;

0 commit comments

Comments
 (0)