Skip to content

Commit 46c4c53

Browse files
authored
Fix pathless route's match when parent is null (#5964)
* Fix pathless route's match when parent is null The logic for this has been moved to matchPath. When automatic path resolving is added, matchPath will need access to the parent match, so we will need to pass that argument to matchPath eventually anyways. When a parent match is null, a match object with default values is returned by matchPath. This match object should not be relied upon for resolving paths, but it will allow a <Route> that uses the component or render prop to render. Fixes #4695 * Make the default match obvious * Add some match docs about null match * Pathless child routes inherit parent, even when null This is only an issue when you use <Route children>. If you render a pathless <Route> inside of a <Route children>, you must also use <Route children> (instead of <Route render> or <Route component>). This commit also switches withRouter to using <Route children> instead of <Route render> so that it works when the parent match is null. * Clarify docs * Fix prop-types usage and format with Prettier
1 parent ecc9a73 commit 46c4c53

File tree

8 files changed

+128
-22
lines changed

8 files changed

+128
-22
lines changed

packages/react-router/docs/api/match.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,28 @@ You'll have access `match` objects in various places:
1717

1818
If a Route does not have a `path`, and therefore always matches, you'll get the closest parent match. Same goes for `withRouter`.
1919

20+
## null matches
21+
22+
A `<Route>` that uses the `children` prop will call its `children` function even when the route's `path` does not match the current location. When this is the case, the `match` will be `null`. Being able to render a `<Route>`'s contents when it does match can be useful, but certain challenges arise from this situation.
23+
24+
The default way to "resolve" URLs is to join the `match.url` string to the "relative" path.
25+
26+
```js
27+
`${match.url}/relative-path`
28+
```
29+
30+
If you attempt to do this when the match is `null`, you will end up with a `TypeError`. This means that it is considered unsafe to attempt to join "relative" paths inside of a `<Route>` when using the `children` prop.
31+
32+
A similar, but more subtle situation occurs when you use a pathless `<Route>` inside of a `<Route>` that generates a `null` match object.
33+
34+
```js
35+
// location.pathname = '/matches'
36+
<Route path='/does-not-match' children={({ match }) => (
37+
// match === null
38+
<Route render={({ match:pathlessMatch }) => (
39+
// pathlessMatch === ???
40+
)}/>
41+
)}/>
42+
```
43+
44+
Pathless `<Route>`s inherit their `match` object from their parent. If their parent `match` is `null`, then their match will also be `null`. This means that a) any child routes/links will have to be absolute because there is no parent to resolve with and b) a pathless route whose parent `match` can be `null` will need to use the `children` prop to render.

packages/react-router/modules/Route.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,7 @@ class Route extends React.Component {
6464
const { route } = router;
6565
const pathname = (location || route.location).pathname;
6666

67-
return path
68-
? matchPath(pathname, { path, strict, exact, sensitive })
69-
: route.match;
67+
return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
7068
}
7169

7270
componentWillMount() {

packages/react-router/modules/Switch.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@ class Switch extends React.Component {
5656
const path = pathProp || from;
5757

5858
child = element;
59-
match = path
60-
? matchPath(location.pathname, { path, exact, strict, sensitive })
61-
: route.match;
59+
match = matchPath(
60+
location.pathname,
61+
{ path, exact, strict, sensitive },
62+
route.match
63+
);
6264
}
6365
});
6466

packages/react-router/modules/__tests__/Route-test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from "react";
22
import ReactDOM from "react-dom";
3+
import PropTypes from "prop-types";
34
import { createMemoryHistory } from "history";
45
import MemoryRouter from "../MemoryRouter";
56
import Router from "../Router";
@@ -388,3 +389,49 @@ describe("A <Route location>", () => {
388389
});
389390
});
390391
});
392+
393+
describe("A pathless <Route>", () => {
394+
let rootContext;
395+
const ContextChecker = (props, context) => {
396+
rootContext = context;
397+
return null;
398+
};
399+
400+
ContextChecker.contextTypes = {
401+
router: PropTypes.object
402+
};
403+
404+
afterEach(() => {
405+
rootContext = undefined;
406+
});
407+
408+
it("inherits its parent match", () => {
409+
const node = document.createElement("div");
410+
ReactDOM.render(
411+
<MemoryRouter initialEntries={["/somepath"]}>
412+
<Route component={ContextChecker} />
413+
</MemoryRouter>,
414+
node
415+
);
416+
417+
const { match } = rootContext.router.route;
418+
expect(match.path).toBe("/");
419+
expect(match.url).toBe("/");
420+
expect(match.isExact).toBe(false);
421+
expect(match.params).toEqual({});
422+
});
423+
424+
it("does not render when parent match is null", () => {
425+
const node = document.createElement("div");
426+
ReactDOM.render(
427+
<MemoryRouter initialEntries={["/somepath"]}>
428+
<Route
429+
path="/no-match"
430+
children={() => <Route component={ContextChecker} />}
431+
/>
432+
</MemoryRouter>,
433+
node
434+
);
435+
expect(rootContext).toBe(undefined);
436+
});
437+
});

packages/react-router/modules/__tests__/matchPath-test.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,21 @@ describe("matchPath", () => {
5555
});
5656

5757
describe("with no path", () => {
58-
it("matches the root URL", () => {
59-
const match = matchPath("/test-location/7", {});
60-
expect(match).toMatchObject({
61-
url: "/",
62-
path: "/",
63-
params: {},
64-
isExact: false
65-
});
58+
it("returns parent match", () => {
59+
const parentMatch = {
60+
url: "/test-location/7",
61+
path: "/test-location/:number",
62+
params: { number: 7 },
63+
isExact: true
64+
};
65+
const match = matchPath("/test-location/7", {}, parentMatch);
66+
expect(match).toBe(parentMatch);
67+
});
68+
69+
it("returns null when parent match is null", () => {
70+
const pathname = "/some/path";
71+
const match = matchPath(pathname, {}, null);
72+
expect(match).toBe(null);
6673
});
6774
});
6875

packages/react-router/modules/__tests__/withRouter-test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,35 @@ describe("withRouter", () => {
4949
);
5050
});
5151

52+
it("works when parent match is null", () => {
53+
let injectedProps;
54+
let parentMatch;
55+
56+
const PropChecker = props => {
57+
injectedProps = props;
58+
return null;
59+
};
60+
61+
const WrappedPropChecker = withRouter(PropChecker);
62+
63+
const node = document.createElement("div");
64+
ReactDOM.render(
65+
<MemoryRouter initialEntries={["/somepath"]}>
66+
<Route
67+
path="/no-match"
68+
children={({ match }) => {
69+
parentMatch = match;
70+
return <WrappedPropChecker />;
71+
}}
72+
/>
73+
</MemoryRouter>,
74+
node
75+
);
76+
77+
expect(parentMatch).toBe(null);
78+
expect(injectedProps.match).toBe(null);
79+
});
80+
5281
describe("inside a <StaticRouter>", () => {
5382
it("provides the staticContext prop", () => {
5483
const PropsChecker = withRouter(props => {

packages/react-router/modules/matchPath.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,13 @@ const compilePath = (pattern, options) => {
2525
/**
2626
* Public API for matching a URL pathname to a path pattern.
2727
*/
28-
const matchPath = (pathname, options = {}) => {
28+
const matchPath = (pathname, options = {}, parent) => {
2929
if (typeof options === "string") options = { path: options };
3030

31-
const {
32-
path = "/",
33-
exact = false,
34-
strict = false,
35-
sensitive = false
36-
} = options;
31+
const { path, exact = false, strict = false, sensitive = false } = options;
32+
33+
if (path == null) return parent;
34+
3735
const { re, keys } = compilePath(path, { end: exact, strict, sensitive });
3836
const match = re.exec(pathname);
3937

packages/react-router/modules/withRouter.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const withRouter = Component => {
1111
const { wrappedComponentRef, ...remainingProps } = props;
1212
return (
1313
<Route
14-
render={routeComponentProps => (
14+
children={routeComponentProps => (
1515
<Component
1616
{...remainingProps}
1717
{...routeComponentProps}

0 commit comments

Comments
 (0)