Skip to content

Commit 1f01079

Browse files
committed
Remove subscriptions
This commit builds upon (and supersedes) the work done by @ryanflorence in #4598 to remove subscriptions from the codebase. Instead of subscribing to location changes and using forceUpdate inside every <Route>, we better follow React's model by using setState in <Router> and letting changes trickle down to descendants using React's built-in state propagation mechanism. This also means that we don't mutate context.router anymore (no more Object.assign). Instead, context.router was split into two parts: context.history and context.route (the <Route> props + match). This allows us to leave context.history untouched and change context.route only according to the props + match state of each <Route>. withRouter was also updated to provide everything in a "router" prop instead of spreading all props to the component.
1 parent 7ba62ea commit 1f01079

File tree

21 files changed

+171
-247
lines changed

21 files changed

+171
-247
lines changed

packages/react-router-dom/modules/Link.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ const isModifiedEvent = (event) =>
44
!!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)
55

66
/**
7-
* The public API for rendering a router-aware <a>.
7+
* The public API for rendering a history-aware <a>.
88
*/
99
class Link extends React.Component {
1010
static contextTypes = {
11-
router: PropTypes.shape({
11+
history: PropTypes.shape({
1212
push: PropTypes.func.isRequired,
1313
replace: PropTypes.func.isRequired,
1414
createHref: PropTypes.func.isRequired
@@ -41,21 +41,21 @@ class Link extends React.Component {
4141
) {
4242
event.preventDefault()
4343

44-
const { router } = this.context
44+
const { history } = this.context
4545
const { replace, to } = this.props
4646

4747
if (replace) {
48-
router.replace(to)
48+
history.replace(to)
4949
} else {
50-
router.push(to)
50+
history.push(to)
5151
}
5252
}
5353
}
5454

5555
render() {
5656
const { replace, to, ...props } = this.props // eslint-disable-line no-unused-vars
5757

58-
const href = this.context.router.createHref(
58+
const href = this.context.history.createHref(
5959
typeof to === 'string' ? { pathname: to } : to
6060
)
6161

packages/react-router-dom/modules/__tests__/BrowserRouter-test.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,25 @@ import ReactDOM from 'react-dom'
44
import BrowserRouter from '../BrowserRouter'
55

66
describe('A <BrowserRouter>', () => {
7-
it('puts a router on context', () => {
8-
let router
9-
const RouterSubject = (props, context) => {
10-
router = context.router
7+
it('puts history on context', () => {
8+
let history
9+
const ContextChecker = (props, context) => {
10+
history = context.history
1111
return null
1212
}
1313

14-
RouterSubject.contextTypes = {
15-
router: PropTypes.object.isRequired
14+
ContextChecker.contextTypes = {
15+
history: PropTypes.object.isRequired
1616
}
1717

1818
const node = document.createElement('div')
1919

2020
ReactDOM.render((
2121
<BrowserRouter>
22-
<RouterSubject/>
22+
<ContextChecker/>
2323
</BrowserRouter>
2424
), node)
2525

26-
expect(router).toBeAn('object')
26+
expect(history).toBeAn('object')
2727
})
2828
})

packages/react-router-dom/modules/__tests__/HashRouter-test.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,25 @@ import ReactDOM from 'react-dom'
44
import HashRouter from '../HashRouter'
55

66
describe('A <HashRouter>', () => {
7-
it('puts a router on context', () => {
8-
let router
9-
const RouterSubject = (props, context) => {
10-
router = context.router
7+
it('puts history on context', () => {
8+
let history
9+
const ContextChecker = (props, context) => {
10+
history = context.history
1111
return null
1212
}
1313

14-
RouterSubject.contextTypes = {
15-
router: PropTypes.object.isRequired
14+
ContextChecker.contextTypes = {
15+
history: PropTypes.object.isRequired
1616
}
1717

1818
const node = document.createElement('div')
1919

2020
ReactDOM.render((
2121
<HashRouter>
22-
<RouterSubject/>
22+
<ContextChecker/>
2323
</HashRouter>
2424
), node)
2525

26-
expect(router).toBeAn('object')
26+
expect(history).toBeAn('object')
2727
})
2828
})

packages/react-router-dom/modules/__tests__/NavLink-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe('NavLink', () => {
3232
to='/pizza'
3333
style={defaultStyle}
3434
activeStyle={activeStyle}
35-
>
35+
>
3636
Pizza!
3737
</NavLink>
3838
</MemoryRouter>

packages/react-router-website/modules/components/APIDocs.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ const docs = [
4848
{ name: 'withRouter',
4949
html: require('../../../react-router/docs/withRouter.md')
5050
},
51-
{ name: 'context.router',
52-
html: require('../../../react-router/docs/context.router.md')
51+
{ name: 'context.history',
52+
html: require('../../../react-router/docs/context.history.md')
5353
},
5454
{ name: 'history',
5555
html: require('../../../react-router/docs/history.md')

packages/react-router/docs/Route.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ You should use only one of these props on a given `<Route>`. See their explanati
2323

2424
## component: func
2525

26-
A React component to render when the location matches. The component receives all the properties on [`context.router`](context.router.md).
26+
A React component to render when the location matches. The component receives all the properties of the [`history`](history.md) object as well as the [`match`](match.md) object that describes how the route matched the current `location`.
2727

2828
```js
2929
<Route path="/user/:username" component={User}/>
@@ -39,7 +39,7 @@ When you use `component` (instead of `render`, below) the router uses [`React.cr
3939

4040
## render: func
4141

42-
Instead of having a new [React element](https://facebook.github.io/react/docs/rendering-elements.html) created for you using the [`component`](#component-func) prop, you can pass in a function to be called when the location matches. The `render` prop receives all the properties of [`context.router`](context.router.md) in a single object.
42+
Instead of having a new [React element](https://facebook.github.io/react/docs/rendering-elements.html) created for you using the [`component`](#component-func) prop, you can pass in a function to be called when the location matches. The `render` prop receives all the properties of the [`history`](history.md) object and the [`match`](match.md) in a single object (same props as the `component` receives).
4343

4444
This allows for convenient inline match rendering and wrapping.
4545

@@ -65,7 +65,7 @@ const FadingRoute = ({ component: Component, ...rest }) => (
6565

6666
Sometimes you need to render whether the path matches the location or not. In these cases, you can use the function `children` prop. It works exactly like `render` except that it gets called whether there is a match or not.
6767

68-
The `children` prop will be called with an object that contains all the properties on [`context.router`](context.router.md). If a route fails to match the URL, the `match` prop will be `null`. This allows you to dynamically adjust your UI based on if the route matches or not.
68+
The `children` prop will be called with an object that contains all the properties of the [`history`](history.md) object. If a route fails to match the URL, the `match` prop will be `null`. This allows you to dynamically adjust your UI based on if the route matches or not.
6969

7070
Here we're adding an `active` class if the route matches
7171

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# context.history
2+
3+
Every [`<Router>`](Router.md) puts its [`history`](history.md) object on [`context`](https://facebook.github.io/react/docs/context.html) as `context.history`. This object is used internally to open a channel of communication between e.g. a `<Router>` and its descendant [`<Route>`](Route.md)s, [`<Link>`](../../react-router-dom/docs/Link.md)s, and [`<Prompt>`](Prompt.md)s, etc.
4+
5+
`context.history` is also occasionally useful as public API, for example, in instances when you need to access the router's imperative API directly. However, we encourage you to use `context.history` only as a last resort. Context itself is an experimental API and may break in a future release of React.
6+
7+
All of the rendering methods in [`<Route>`](Route.md) receive all the properties on `context.history`, so you shouldn't ever need to access it directly for rendering. Also, you can navigate (`push` or `replace`) using a [`<Redirect>`](Redirect.md) element.

packages/react-router/docs/context.router.md

Lines changed: 0 additions & 23 deletions
This file was deleted.

packages/react-router/docs/withRouter.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# withRouter
22

3-
You can get access to the [`router`](context.router.md)'s properties via the `withRouter` higher-order component. This is the recommended way to access the `router` object. `withRouter` will re-render its component every time the route changes.
3+
You can get access to the [`history`](history.md) object's properties and the closest [`<Route>`](Route.md)'s [`match`](match.md) via the `withRouter` higher-order component. `withRouter` will re-render its component every time the route changes with a single `router` prop.
44

55
```js
66
import React, { PropTypes } from 'react'
@@ -9,12 +9,14 @@ import { withRouter } from 'react-router'
99
// A simple component that shows the pathname of the current location
1010
class ShowTheLocation extends React.Component {
1111
static propTypes = {
12-
location: PropTypes.object.isRequired
12+
router: PropTypes.object.isRequired
1313
}
1414

1515
render() {
16+
const { router } = this.props
17+
1618
return (
17-
<div>You are now at {this.props.location.pathname}</div>
19+
<div>You are now at {router.location.pathname}</div>
1820
)
1921
}
2022
}

packages/react-router/modules/.babelrc

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
"stage-1",
55
"react"
66
],
7-
"plugins": [
8-
"transform-object-assign"
9-
],
107
"env": {
118
"production": {
129
"plugins": [

packages/react-router/modules/Prompt.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import React, { PropTypes } from 'react'
66
*/
77
class Prompt extends React.Component {
88
static contextTypes = {
9-
router: PropTypes.shape({
9+
history: PropTypes.shape({
1010
block: PropTypes.func.isRequired
1111
}).isRequired
1212
}
@@ -27,7 +27,7 @@ class Prompt extends React.Component {
2727
if (this.unblock)
2828
this.unblock()
2929

30-
this.unblock = this.context.router.block(message)
30+
this.unblock = this.context.history.block(message)
3131
}
3232

3333
disable() {

packages/react-router/modules/Redirect.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import React, { PropTypes } from 'react'
66
*/
77
class Redirect extends React.Component {
88
static contextTypes = {
9-
router: PropTypes.shape({
9+
history: PropTypes.shape({
1010
push: PropTypes.func.isRequired,
1111
replace: PropTypes.func.isRequired,
1212
staticContext: PropTypes.object
@@ -26,23 +26,23 @@ class Redirect extends React.Component {
2626
}
2727

2828
componentWillMount() {
29-
if (this.context.router.staticContext)
29+
if (this.context.history.staticContext)
3030
this.perform()
3131
}
3232

3333
componentDidMount() {
34-
if (!this.context.router.staticContext)
34+
if (!this.context.history.staticContext)
3535
this.perform()
3636
}
3737

3838
perform() {
39-
const { router } = this.context
39+
const { history } = this.context
4040
const { push, to } = this.props
4141

4242
if (push) {
43-
router.push(to)
43+
history.push(to)
4444
} else {
45-
router.replace(to)
45+
history.replace(to)
4646
}
4747
}
4848

packages/react-router/modules/Route.js

Lines changed: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
1-
import React, { PropTypes } from 'react'
21
import warning from 'warning'
2+
import React, { PropTypes } from 'react'
33
import matchPath from './matchPath'
44

5-
const computeMatch = (router, { location, computedMatch, path, exact, strict }) =>
6-
computedMatch || matchPath((location || router.location).pathname, { path, exact, strict })
7-
85
/**
96
* The public API for matching a single path and rendering.
107
*/
118
class Route extends React.Component {
129
static contextTypes = {
13-
router: PropTypes.shape({
14-
listen: PropTypes.func.isRequired
15-
}).isRequired
10+
history: PropTypes.object.isRequired
1611
}
1712

1813
static propTypes = {
@@ -30,61 +25,57 @@ class Route extends React.Component {
3025
}
3126

3227
static childContextTypes = {
33-
router: PropTypes.object.isRequired
28+
route: PropTypes.object.isRequired
3429
}
3530

3631
getChildContext() {
3732
return {
38-
router: this.router
33+
route: {
34+
...this.props,
35+
match: this.state.match
36+
}
3937
}
4038
}
4139

42-
componentWillMount() {
43-
const parentRouter = this.context.router
40+
state = {
41+
match: this.computeMatch(this.props)
42+
}
4443

45-
this.router = {
46-
...parentRouter,
47-
match: computeMatch(parentRouter, this.props)
48-
}
44+
computeMatch({ computedMatch, location, path, strict, exact }) {
45+
if (computedMatch)
46+
return computedMatch // <Switch> already computed the match for us
4947

50-
// Start listening here so we can <Redirect> on the initial render.
51-
this.unlisten = parentRouter.listen(() => {
52-
Object.assign(this.router, parentRouter, {
53-
match: computeMatch(parentRouter, this.props)
54-
})
48+
const pathname = (location || this.context.history.location).pathname
5549

56-
this.forceUpdate()
57-
})
50+
return matchPath(pathname, { path, strict, exact })
5851
}
5952

6053
componentWillReceiveProps(nextProps) {
61-
Object.assign(this.router, {
62-
match: computeMatch(this.router, nextProps)
63-
})
64-
6554
warning(
6655
!(nextProps.location && !this.props.location),
67-
'You cannot change from an uncontrolled to controlled Route. You passed in a `location` prop on a re-render when initially there was none.'
56+
'<Route> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
6857
)
58+
6959
warning(
7060
!(!nextProps.location && this.props.location),
71-
'You cannot change from a controlled to an uncontrolled Route. You passed in a `location` prop initially but on a re-render there was none.'
61+
'<Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
7262
)
73-
}
7463

75-
componentWillUnmount() {
76-
this.unlisten()
64+
this.setState({
65+
match: this.computeMatch(nextProps)
66+
})
7767
}
7868

7969
render() {
70+
const { match } = this.state
8071
const { children, component, render } = this.props
81-
const props = { ...this.router }
72+
const props = { ...this.context.history, match }
8273

8374
return (
8475
component ? ( // component prop gets first priority, only called if there's a match
85-
props.match ? React.createElement(component, props) : null
76+
match ? React.createElement(component, props) : null
8677
) : render ? ( // render prop is next, only called if there's a match
87-
props.match ? render(props) : null
78+
match ? render(props) : null
8879
) : children ? ( // children come last, always called
8980
typeof children === 'function' ? (
9081
children(props)

0 commit comments

Comments
 (0)