Skip to content

Commit eac8217

Browse files
aaugustinryanflorence
authored andcommitted
Add <Link replace> and <Redirect push> (#3912)
fixes #3903
1 parent e5be03a commit eac8217

File tree

5 files changed

+133
-19
lines changed

5 files changed

+133
-19
lines changed

modules/Link.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66

77
class Link extends React.Component {
88
static defaultProps = {
9+
replace: false,
910
activeOnlyWhenExact: false,
1011
className: '',
1112
activeClassName: '',
@@ -38,12 +39,15 @@ class Link extends React.Component {
3839
isLeftClickEvent(event)
3940
) {
4041
event.preventDefault()
41-
this.context.router.transitionTo(this.props.to)
42+
this.handleTransition()
4243
}
4344
}
4445

4546
handleTransition = () => {
46-
this.context.router.transitionTo(this.props.to)
47+
const { router } = this.context
48+
const { to, replace } = this.props
49+
const navigate = replace ? router.replaceWith : router.transitionTo
50+
navigate(to)
4751
}
4852

4953
render() {
@@ -107,6 +111,7 @@ class Link extends React.Component {
107111
if (__DEV__) {
108112
Link.propTypes = {
109113
to: PropTypes.oneOfType([ PropTypes.string, PropTypes.object ]).isRequired,
114+
replace: PropTypes.bool,
110115
activeStyle: PropTypes.object,
111116
activeClassName: PropTypes.string,
112117
location: PropTypes.object,

modules/Redirect.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import {
44
} from './PropTypes'
55

66
class Redirect extends React.Component {
7+
static defaultProps = {
8+
push: false
9+
}
10+
711
static contextTypes = {
812
router: routerContextType,
913
serverRouter: PropTypes.object
@@ -20,9 +24,12 @@ class Redirect extends React.Component {
2024

2125
redirect() {
2226
const { router } = this.context
27+
const { to, push } = this.props
2328
// so that folks can unit test w/o hassle
24-
if (router)
25-
router.replaceWith(this.props.to)
29+
if (router) {
30+
const navigate = push ? router.transitionTo : router.replaceWith
31+
navigate(to)
32+
}
2633
}
2734

2835
render() {
@@ -35,7 +42,8 @@ if (__DEV__) {
3542
to: PropTypes.oneOfType([
3643
PropTypes.string,
3744
PropTypes.object
38-
]).isRequired
45+
]).isRequired,
46+
push: PropTypes.bool
3947
}
4048
}
4149

modules/__tests__/integration-test.js

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@ import expect from 'expect'
22
import React from 'react'
33
import Router from '../MemoryRouter'
44
import NavigationPrompt from '../NavigationPrompt'
5+
import Redirect from '../Redirect'
56
import StaticRouter from '../StaticRouter'
67
import Match from '../Match'
78
import Miss from '../Miss'
89
import { Simulate } from 'react-addons-test-utils'
910
import Link from '../Link'
1011
import { render } from 'react-dom'
1112

13+
const requiredPropsForStaticRouter = {
14+
location: '/',
15+
action: 'POP',
16+
createHref: () => {},
17+
blockTransitions: () => {}, // we sure we want this required? servers don't need it.
18+
onPush: () => {},
19+
onReplace: () => {}
20+
}
21+
1222
describe('Integration Tests', () => {
1323

1424
it('renders root match', () => {
@@ -254,6 +264,46 @@ describe('clicking around', () => {
254264
Simulate.click(div.querySelector('#one'), leftClickEvent)
255265
expect(div.innerHTML).toContain(TEXT1)
256266
})
267+
268+
it('pushes a new URL', () => {
269+
const div = document.createElement('div')
270+
const pushes = []
271+
const replaces = []
272+
const TARGET = '/TARGET'
273+
render((
274+
<StaticRouter
275+
{...requiredPropsForStaticRouter}
276+
onPush={(loc) => { pushes.push(loc) }}
277+
onReplace={(loc) => { replaces.push(loc) }}
278+
>
279+
<Link id="target" to={TARGET}>{TARGET}</Link>
280+
</StaticRouter>
281+
), div)
282+
Simulate.click(div.querySelector('#target'), leftClickEvent)
283+
expect(pushes.length).toEqual(1)
284+
expect(pushes[0].pathname).toEqual(TARGET)
285+
expect(replaces.length).toEqual(0)
286+
})
287+
288+
it('replaces the current URL with replace', () => {
289+
const div = document.createElement('div')
290+
const pushes = []
291+
const replaces = []
292+
const TARGET = '/TARGET'
293+
render((
294+
<StaticRouter
295+
{...requiredPropsForStaticRouter}
296+
onPush={(loc) => { pushes.push(loc) }}
297+
onReplace={(loc) => { replaces.push(loc) }}
298+
>
299+
<Link id="target" to={TARGET} replace>{TARGET}</Link>
300+
</StaticRouter>
301+
), div)
302+
Simulate.click(div.querySelector('#target'), leftClickEvent)
303+
expect(pushes.length).toEqual(0)
304+
expect(replaces.length).toEqual(1)
305+
expect(replaces[0].pathname).toEqual(TARGET)
306+
})
257307
})
258308

259309
describe('Link location descriptors', () => {
@@ -322,23 +372,14 @@ describe('Link with a query', () => {
322372

323373
describe('Match and Miss Integration', () => {
324374

325-
const requiredProps = {
326-
location: '/',
327-
action: 'POP',
328-
createHref: () => {},
329-
blockTransitions: () => {}, // we sure we want this required? servers don't need it.
330-
onPush: () => {},
331-
onReplace: () => {}
332-
}
333-
334375
describe('Miss', () => {
335376
it('renders when nothing else matches', () => {
336377
const div = document.createElement('div')
337378
const FOO = '/FOO'
338379
const MISS = '/MISS'
339380
render((
340381
<StaticRouter
341-
{...requiredProps}
382+
{...requiredPropsForStaticRouter}
342383
location={{ pathname: MISS }}
343384
>
344385
<div>
@@ -357,7 +398,7 @@ describe('Match and Miss Integration', () => {
357398
const MISS = '/MISS'
358399
render((
359400
<StaticRouter
360-
{...requiredProps}
401+
{...requiredPropsForStaticRouter}
361402
location={{ pathname: FOO }}
362403
>
363404
<div>
@@ -402,3 +443,43 @@ describe('NavigationPrompt', () => {
402443
expect(message).toEqual(TEXT)
403444
})
404445
})
446+
447+
describe('Redirect', () => {
448+
it('replaces the current URL', () => {
449+
const div = document.createElement('div')
450+
const pushes = []
451+
const replaces = []
452+
const REDIRECTED = '/REDIRECTED'
453+
render((
454+
<StaticRouter
455+
{...requiredPropsForStaticRouter}
456+
onPush={(loc) => { pushes.push(loc) }}
457+
onReplace={(loc) => { replaces.push(loc) }}
458+
>
459+
<Redirect to={REDIRECTED} />
460+
</StaticRouter>
461+
), div)
462+
expect(pushes.length).toEqual(0)
463+
expect(replaces.length).toEqual(1)
464+
expect(replaces[0].pathname).toEqual(REDIRECTED)
465+
})
466+
467+
it('pushes a new URL with push', () => {
468+
const div = document.createElement('div')
469+
const pushes = []
470+
const replaces = []
471+
const REDIRECTED = '/REDIRECTED'
472+
render((
473+
<StaticRouter
474+
{...requiredPropsForStaticRouter}
475+
onPush={(loc) => { pushes.push(loc) }}
476+
onReplace={(loc) => { replaces.push(loc) }}
477+
>
478+
<Redirect to={REDIRECTED} push />
479+
</StaticRouter>
480+
), div)
481+
expect(pushes.length).toEqual(1)
482+
expect(pushes[0].pathname).toEqual(REDIRECTED)
483+
expect(replaces.length).toEqual(0)
484+
})
485+
})

website/api/Link.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Children function parameter is an object with the following keys:
2424

2525
```js
2626
<Link to="/courses">{
27-
({isActive, location, href, onClick, transition}) =>
27+
({isActive, location, href, onClick, transition}) =>
2828
<RaisedButton label="Courses" onClick={onClick} primary={isActive} href={href} />
2929
}</Link>
3030
```
@@ -120,4 +120,13 @@ in the hierarchy.
120120
<Match pattern="/foo" location={this.props.location}/>
121121
```
122122

123+
## `replace: bool`
124+
125+
When true, clicking the link will replace the current history state with
126+
`replaceState` instead of adding a new history state with `pushState`.
127+
128+
```js
129+
<Link to="/courses" replace/>
130+
```
131+
123132
# `</Link>`

website/api/Redirect.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# `<Redirect>`
22

3-
Rendering a `Redirect` will navigate to a new location and add the
4-
previous location onto the next location state.
3+
Rendering a `Redirect` will navigate to a new location.
4+
5+
The new location will override the current location in the browser's history,
6+
like server-side redirects (HTTP 3xx) do.
57

68
(If this freaks you out you can use the imperative API from the `router`
79
on context.)
@@ -37,4 +39,13 @@ A location descriptor to redirect to.
3739
}}/>
3840
```
3941

42+
## `push: bool`
43+
44+
When true, redirecting will add a new history state with `pushState` instead
45+
of replacing the current history state with `replaceState`.
46+
47+
```js
48+
<Redirect to="/somewhere/else" push/>
49+
```
50+
4051
# `</Redirect>`

0 commit comments

Comments
 (0)