Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion packages/react-router/modules/Route.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,51 @@ function evalChildrenDev(children, props, path) {
return value || null;
}

function deepEqual(x, y) {
if (x === y) return true;

if (
typeof x === "object" &&
x !== null &&
typeof y === "object" &&
y !== null
) {
const xKeys = Object.keys(x);
const yKeys = Object.keys(y);

if (xKeys.length !== yKeys.length) return false;

for (const key of xKeys) {
if (!(yKeys.includes(key) && deepEqual(x[key], y[key]))) return false;
}

return true;
}

return false;
}

/**
* The public API for matching a single path and rendering.
*/
class Route extends React.Component {
_prevMatch = null;

render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Route> outside a <Router>");

const location = this.props.location || context.location;
const match = this.props.computedMatch
let match = this.props.computedMatch
? this.props.computedMatch // <Switch> already computed the match for us
: this.props.path
? matchPath(location.pathname, this.props)
: context.match;
// Reuse the same object if possible so that optimizations like React.PureComponent or React.memo won't be broken
match = deepEqual(match, this._prevMatch) ? this._prevMatch : match;
this._prevMatch = match;

const props = { ...context, location, match };

Expand Down
41 changes: 40 additions & 1 deletion packages/react-router/modules/__tests__/Route-test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React from "react";
import React, { useState, useEffect, memo } from "react";
import ReactDOM from "react-dom";
import { createMemoryHistory as createHistory } from "history";
import { MemoryRouter, Router, Route } from "react-router";

import renderStrict from "./utils/renderStrict.js";
import { act } from "react-dom/test-utils";

describe("A <Route>", () => {
const node = document.createElement("div");
Expand Down Expand Up @@ -536,6 +537,44 @@ describe("A <Route>", () => {

expect(console.error).not.toHaveBeenCalled();
});

it("should not break optimizations like React.PureComponent or React.memo by passing a different object every time", () => {
const SubRoute = jest.fn(function SubRoute(props) {
return (
<div>
some very expensive contents for subpath:
{props.match.params.subPath}
</div>
);
});
const MemorizedSubRoute = memo(SubRoute);

function MainRoute() {
const [text, setText] = useState("default text");
useEffect(() => {
setText("updated text");
}, []);

return (
<div>
<div>some common widget with text: {text}</div>
<Route path="/:subpath" component={MemorizedSubRoute} />
</div>
);
}

act(() => {
renderStrict(
<MemoryRouter initialEntries={["/one"]}>
<Route path="/" component={MainRoute} />
</MemoryRouter>,
node
);
});

// When MainRoute updates, but the URL haven't changed, should not pass a different props.match object and force SubRoute to re-render
expect(SubRoute).toHaveBeenCalledTimes(1);
});
});

describe("the `render` prop", () => {
Expand Down