Skip to content

Commit b9be9a9

Browse files
pshrmnmjackson
authored andcommitted
Add context.route.location (#4589)
* Add context.route.location The default `context.route.location` value is the same as `context.history.location`. However, if a <Route> has a `location` prop, then all of its children will see that location object as `context.route.location`. This also updates the `<Switch>` component to pass its location as a prop to the component that it renders. * Add a modal example * Pass correct location to Route's rendered component * pass context to Route's computeMatch When calling computeMatch from componentWillReceiveProps, we need to use nextContext, not this.context * Fix websites example to work with new Route prop signature
1 parent 82afbdd commit b9be9a9

File tree

9 files changed

+298
-34
lines changed

9 files changed

+298
-34
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ const EXAMPLES = [
6161
path: '/examples/route-config',
6262
load: require('bundle?lazy!babel!../examples/RouteConfig'),
6363
loadSource: require('bundle?lazy!!prismjs?lang=jsx!../examples/RouteConfig.js')
64+
},
65+
{ name: 'Modal Gallery',
66+
path: '/examples/modal-gallery',
67+
load: require('bundle?lazy!babel!../examples/ModalGallery'),
68+
loadSource: require('bundle?lazy!!prismjs?lang=jsx!../examples/ModalGallery.js')
6469
}
6570
]
6671

packages/react-router-website/modules/examples/Auth.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ const fakeAuth = {
4040
}
4141
}
4242

43-
const AuthButton = withRouter(({ push }) => (
43+
const AuthButton = withRouter(({ history }) => (
4444
fakeAuth.isAuthenticated ? (
4545
<p>
4646
Welcome! <button onClick={() => {
47-
fakeAuth.signout(() => push('/'))
47+
fakeAuth.signout(() => history.push('/'))
4848
}}>Sign out</button>
4949
</p>
5050
) : (
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import React from 'react'
2+
import {
3+
BrowserRouter as Router,
4+
Switch,
5+
Route,
6+
Link
7+
} from 'react-router-dom'
8+
9+
10+
const IMAGES = [
11+
{ id: 0, title: 'Dark Orchid', color: 'DarkOrchid' },
12+
{ id: 1, title: 'Lime Green', color: 'LimeGreen' },
13+
{ id: 2, title: 'Gold', color: 'Gold' },
14+
{ id: 3, title: 'Midnight Blue', color: 'MidnightBlue' },
15+
{ id: 4, title: 'Dark Slate Gray', color: 'DarkSlateGray' },
16+
{ id: 5, title: 'Tomato', color: 'Tomato' },
17+
{ id: 6, title: 'Seven Ate Nine', color: '#789' },
18+
{ id: 7, title: 'Olive Drab', color: 'OliveDrab' },
19+
{ id: 8, title: 'Crimson', color: 'Crimson' },
20+
{ id: 9, title: 'Sea Green', color: 'SeaGreen' }
21+
]
22+
23+
const Thumbnail = ({ color }) =>
24+
<div style={{ width: 50, height: 50, background: color }}></div>
25+
26+
const Image = ({ color }) =>
27+
<div style={{ width: '100%', height: 400, background: color }}></div>
28+
29+
const Home = () => (
30+
<div>
31+
<Link to='/gallery'>Visit the Gallery</Link>
32+
<h2>Featured Images</h2>
33+
<ul>
34+
<li><Link to='/img/5'>Tomato</Link></li>
35+
<li><Link to='/img/9'>Sea Green</Link></li>
36+
</ul>
37+
</div>
38+
)
39+
40+
const Gallery = () => (
41+
<div>
42+
{
43+
IMAGES.map(i => (
44+
<Link
45+
key={i.id}
46+
to={{ pathname: `/img/${i.id}`, state: { modal: true} }}
47+
>
48+
<Thumbnail color={i.color} />
49+
<p>{i.title}</p>
50+
</Link>
51+
))
52+
}
53+
</div>
54+
)
55+
56+
const ImageView = ({ match }) => {
57+
const image = IMAGES[parseInt(match.params.id, 10)]
58+
if (!image) {
59+
return <div>Image not found</div>
60+
}
61+
62+
return (
63+
<div>
64+
<h1>{image.title}</h1>
65+
<Image color={image.color} />
66+
</div>
67+
)
68+
}
69+
70+
const Modal = ({ match, history }) => {
71+
const image = IMAGES[parseInt(match.params.id, 10)]
72+
if (!image) {
73+
return null
74+
}
75+
const back = (e) => {
76+
e.stopPropagation()
77+
history.goBack()
78+
}
79+
return (
80+
<div
81+
onClick={back}
82+
style={{
83+
position: 'absolute',
84+
top: 0,
85+
left: 0,
86+
bottom: 0,
87+
right: 0,
88+
background: 'rgba(0, 0, 0, 0.15)'
89+
}}
90+
>
91+
<div className='modal' style={{
92+
position: 'absolute',
93+
background: '#fff',
94+
top: 25,
95+
left: '10%',
96+
right: '10%',
97+
padding: 15,
98+
border: '2px solid #444'
99+
}}>
100+
<h1>{image.title}</h1>
101+
<Image color={image.color} />
102+
<button type='button' onClick={back}>
103+
Close
104+
</button>
105+
</div>
106+
</div>
107+
)
108+
}
109+
110+
class ModalSwitch extends React.Component {
111+
112+
componentWillMount() {
113+
// set the initial previousLocation value on mount
114+
this.previousLocation = this.props.location
115+
}
116+
117+
componentWillUpdate(nextProps) {
118+
const { location } = this.props
119+
// set previousLocation if props.location is not modal
120+
if (nextProps.history.action !== 'POP' && (!location.state || !location.state.modal)) {
121+
this.previousLocation = this.props.location
122+
}
123+
}
124+
125+
render() {
126+
const { location } = this.props
127+
const isModal = !!(
128+
location.state &&
129+
location.state.modal &&
130+
this.previousLocation !== location
131+
)
132+
return (
133+
<div>
134+
<Switch location={isModal ? this.previousLocation : location}>
135+
<Route exact path='/' component={Home}/>
136+
<Route path='/gallery' component={Gallery}/>
137+
<Route path='/img/:id' component={ImageView}/>
138+
</Switch>
139+
{ isModal ? <Route path='/img/:id' component={Modal} /> : null}
140+
</div>
141+
)
142+
}
143+
}
144+
145+
const ModalGallery = () => (
146+
<Router>
147+
<Route component={ModalSwitch} />
148+
</Router>
149+
)
150+
151+
export default ModalGallery

packages/react-router/modules/Route.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import matchPath from './matchPath'
77
*/
88
class Route extends React.Component {
99
static contextTypes = {
10-
history: PropTypes.object.isRequired
10+
history: PropTypes.object.isRequired,
11+
route: PropTypes.object.isRequired
1112
}
1213

1314
static propTypes = {
@@ -31,26 +32,26 @@ class Route extends React.Component {
3132
getChildContext() {
3233
return {
3334
route: {
34-
location: this.props.location || this.context.history.location,
35+
location: this.props.location || this.context.route.location,
3536
match: this.state.match
3637
}
3738
}
3839
}
3940

4041
state = {
41-
match: this.computeMatch(this.props)
42+
match: this.computeMatch(this.props, this.context)
4243
}
4344

44-
computeMatch({ computedMatch, location, path, strict, exact }) {
45+
computeMatch({ computedMatch, location, path, strict, exact }, { route }) {
4546
if (computedMatch)
4647
return computedMatch // <Switch> already computed the match for us
4748

48-
const pathname = (location || this.context.history.location).pathname
49+
const pathname = (location || route.location).pathname
4950

5051
return matchPath(pathname, { path, strict, exact })
5152
}
5253

53-
componentWillReceiveProps(nextProps) {
54+
componentWillReceiveProps(nextProps, nextContext) {
5455
warning(
5556
!(nextProps.location && !this.props.location),
5657
'<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.'
@@ -62,15 +63,15 @@ class Route extends React.Component {
6263
)
6364

6465
this.setState({
65-
match: this.computeMatch(nextProps)
66+
match: this.computeMatch(nextProps, nextContext)
6667
})
6768
}
6869

6970
render() {
7071
const { match } = this.state
7172
const { children, component, render } = this.props
72-
const { history } = this.context
73-
const { location } = history
73+
const { history, route } = this.context
74+
const location = this.props.location || route.location
7475
const props = { match, location, history }
7576

7677
return (

packages/react-router/modules/Router.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class Router extends React.Component {
2020
return {
2121
history: this.props.history,
2222
route: {
23+
location: this.props.history.location,
2324
match: this.state.match
2425
}
2526
}

packages/react-router/modules/Switch.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import matchPath from './matchPath'
77
*/
88
class Switch extends React.Component {
99
static contextTypes = {
10-
history: PropTypes.object.isRequired
10+
route: PropTypes.object.isRequired
1111
}
1212

1313
static propTypes = {
@@ -29,7 +29,7 @@ class Switch extends React.Component {
2929

3030
render() {
3131
const { children } = this.props
32-
const location = this.props.location || this.context.history.location
32+
const location = this.props.location || this.context.route.location
3333

3434
let match, child
3535
React.Children.forEach(children, element => {
@@ -39,7 +39,7 @@ class Switch extends React.Component {
3939
}
4040
})
4141

42-
return match ? React.cloneElement(child, { computedMatch: match }) : null
42+
return match ? React.cloneElement(child, { location, computedMatch: match }) : null
4343
}
4444
}
4545

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

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('A <Route>', () => {
3737
expect(node.innerHTML).toNotContain(TEXT)
3838
})
3939

40-
it('can use a `location` prop instead of `context.history.location`', () => {
40+
it('can use a `location` prop instead of `context.route.location`', () => {
4141
const TEXT = 'tamarind chutney'
4242
const node = document.createElement('div')
4343

@@ -125,7 +125,21 @@ describe('A <Route>', () => {
125125
expect(node.innerHTML).toContain(TEXT)
126126
})
127127

128+
it('matches using nextContext when updating', () => {
129+
const node = document.createElement('div')
128130

131+
let push
132+
ReactDOM.render((
133+
<MemoryRouter initialEntries={[ '/sushi/california' ]}>
134+
<Route path="/sushi/:roll" render={({ history, match }) => {
135+
push = history.push
136+
return <div>{match.url}</div>
137+
}}/>
138+
</MemoryRouter>
139+
), node)
140+
push('/sushi/spicy-tuna')
141+
expect(node.innerHTML).toContain('/sushi/spicy-tuna')
142+
})
129143
})
130144

131145
describe('<Route> render props', () => {
@@ -243,3 +257,72 @@ describe('A <Route exact strict>', () => {
243257
expect(node.innerHTML).toNotContain(TEXT)
244258
})
245259
})
260+
261+
describe('A <Route location>', () => {
262+
it('can use a `location` prop instead of `router.location`', () => {
263+
const TEXT = 'tamarind chutney'
264+
const node = document.createElement('div')
265+
266+
ReactDOM.render((
267+
<MemoryRouter initialEntries={[ '/mint' ]}>
268+
<Route
269+
location={{ pathname: '/tamarind' }}
270+
path="/tamarind"
271+
render={() => (
272+
<h1>{TEXT}</h1>
273+
)}
274+
/>
275+
</MemoryRouter>
276+
), node)
277+
278+
expect(node.innerHTML).toContain(TEXT)
279+
})
280+
281+
describe('children', () => {
282+
it('uses parent\'s prop location', () => {
283+
const TEXT = 'cheddar pretzel'
284+
const node = document.createElement('div')
285+
286+
ReactDOM.render((
287+
<MemoryRouter initialEntries={[ '/popcorn' ]}>
288+
<Route
289+
location={{ pathname: '/pretzels/cheddar' }}
290+
path="/pretzels"
291+
render={() => (
292+
<Route path='/pretzels/cheddar' render={() => (
293+
<h1>{TEXT}</h1>
294+
)} />
295+
)}
296+
/>
297+
</MemoryRouter>
298+
), node)
299+
300+
expect(node.innerHTML).toContain(TEXT)
301+
})
302+
303+
it('continues to use parent\'s prop location after navigation', () => {
304+
const TEXT = 'cheddar pretzel'
305+
const node = document.createElement('div')
306+
let push
307+
ReactDOM.render((
308+
<MemoryRouter initialEntries={[ '/popcorn' ]}>
309+
<Route
310+
location={{ pathname: '/pretzels/cheddar' }}
311+
path="/pretzels"
312+
render={({ history }) => {
313+
push = history.push
314+
return (
315+
<Route path='/pretzels/cheddar' render={() => (
316+
<h1>{TEXT}</h1>
317+
)} />
318+
)
319+
}}
320+
/>
321+
</MemoryRouter>
322+
), node)
323+
expect(node.innerHTML).toContain(TEXT)
324+
push('/chips')
325+
expect(node.innerHTML).toContain(TEXT)
326+
})
327+
})
328+
})

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ describe('A <Router>', () => {
9393
expect(rootContext.route.match.url).toEqual('/')
9494
expect(rootContext.route.match.params).toEqual({})
9595
expect(rootContext.route.match.isExact).toEqual(true)
96+
expect(rootContext.route.location).toEqual(history.location)
9697
})
9798

9899
it('updates context.route upon navigation', () => {

0 commit comments

Comments
 (0)