Skip to content

Commit 1b52945

Browse files
committed
Merge pull request #2748 from taion/route-leave-hook
Tear down route leave hooks upon leaving route
2 parents 2def747 + aab3188 commit 1b52945

File tree

2 files changed

+96
-39
lines changed

2 files changed

+96
-39
lines changed

modules/__tests__/transitionHooks-test.js

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,48 @@ import execSteps from './execSteps'
77
import Router from '../Router'
88

99
describe('When a router enters a branch', function () {
10+
let
11+
node, leaveHookSpy, removeLeaveHook,
12+
DashboardRoute, NewsFeedRoute, InboxRoute, RedirectToInboxRoute, MessageRoute,
13+
routes
1014

11-
class Dashboard extends Component {
12-
render() {
13-
return (
14-
<div className="Dashboard">
15-
<h1>The Dashboard</h1>
16-
{this.props.children}
17-
</div>
18-
)
15+
beforeEach(function () {
16+
node = document.createElement('div')
17+
leaveHookSpy = expect.createSpy()
18+
19+
class Dashboard extends Component {
20+
render() {
21+
return (
22+
<div className="Dashboard">
23+
<h1>The Dashboard</h1>
24+
{this.props.children}
25+
</div>
26+
)
27+
}
1928
}
20-
}
2129

22-
class NewsFeed extends Component {
23-
render() {
24-
return <div>News</div>
30+
class NewsFeed extends Component {
31+
componentWillMount() {
32+
removeLeaveHook = this.context.router.addRouteLeaveHook(
33+
this.props.route,
34+
() => leaveHookSpy() // Break reference equality.
35+
)
36+
}
37+
38+
render() {
39+
return <div>News</div>
40+
}
2541
}
26-
}
2742

28-
class Inbox extends Component {
29-
render() {
30-
return <div>Inbox</div>
43+
NewsFeed.contextTypes = {
44+
router: React.PropTypes.object.isRequired
3145
}
32-
}
3346

34-
let node, DashboardRoute, NewsFeedRoute, InboxRoute, RedirectToInboxRoute, MessageRoute, routes
35-
beforeEach(function () {
36-
node = document.createElement('div')
47+
class Inbox extends Component {
48+
render() {
49+
return <div>Inbox</div>
50+
}
51+
}
3752

3853
NewsFeedRoute = {
3954
path: 'news',
@@ -121,6 +136,35 @@ describe('When a router enters a branch', function () {
121136
})
122137
})
123138

139+
it('calls the route leave hooks when leaving the route', function (done) {
140+
const history = createHistory('/news')
141+
142+
// Stub this function to exercise the code path.
143+
history.listenBeforeUnload = () => (() => {})
144+
145+
render(<Router history={history} routes={routes}/>, node, function () {
146+
expect(leaveHookSpy.calls.length).toEqual(0)
147+
history.push('/inbox')
148+
expect(leaveHookSpy.calls.length).toEqual(1)
149+
history.push('/news')
150+
expect(leaveHookSpy.calls.length).toEqual(1)
151+
history.push('/inbox')
152+
expect(leaveHookSpy.calls.length).toEqual(2)
153+
done()
154+
})
155+
})
156+
157+
it('does not call removed route leave hooks', function (done) {
158+
const history = createHistory('/news')
159+
160+
render(<Router history={history} routes={routes}/>, node, function () {
161+
removeLeaveHook()
162+
history.push('/inbox')
163+
expect(leaveHookSpy).toNotHaveBeenCalled()
164+
done()
165+
})
166+
})
167+
124168
describe('and one of the transition hooks navigates to another route', function () {
125169
it('immediately transitions to the new route', function (done) {
126170
const redirectRouteEnterSpy = spyOn(RedirectToInboxRoute, 'onEnter').andCallThrough()

modules/createTransitionManager.js

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ export default function createTransitionManager(history, routes) {
7575

7676
runLeaveHooks(leaveRoutes)
7777

78+
// Tear down confirmation hooks for left routes
79+
leaveRoutes.forEach(removeListenBeforeHooksForRoute)
80+
7881
runEnterHooks(enterRoutes, nextState, function (error, redirectInfo) {
7982
if (error) {
8083
callback(error)
@@ -99,8 +102,8 @@ export default function createTransitionManager(history, routes) {
99102

100103
let RouteGuid = 1
101104

102-
function getRouteID(route) {
103-
return route.__id__ || (route.__id__ = RouteGuid++)
105+
function getRouteID(route, create = true) {
106+
return route.__id__ || create && (route.__id__ = RouteGuid++)
104107
}
105108

106109
const RouteHooks = {}
@@ -141,6 +144,7 @@ export default function createTransitionManager(history, routes) {
141144
})
142145
}
143146

147+
/* istanbul ignore next: untestable with Karma */
144148
function beforeUnloadHook() {
145149
// Synchronously check to see if any route hooks want
146150
// to prevent the current window/tab from closing.
@@ -160,6 +164,28 @@ export default function createTransitionManager(history, routes) {
160164

161165
let unlistenBefore, unlistenBeforeUnload
162166

167+
function removeListenBeforeHooksForRoute(route) {
168+
const routeID = getRouteID(route, false)
169+
if (!routeID) {
170+
return
171+
}
172+
173+
delete RouteHooks[routeID]
174+
175+
if (!hasAnyProperties(RouteHooks)) {
176+
// teardown transition & beforeunload hooks
177+
if (unlistenBefore) {
178+
unlistenBefore()
179+
unlistenBefore = null
180+
}
181+
182+
if (unlistenBeforeUnload) {
183+
unlistenBeforeUnload()
184+
unlistenBeforeUnload = null
185+
}
186+
}
187+
}
188+
163189
/**
164190
* Registers the given hook function to run before leaving the given route.
165191
*
@@ -180,10 +206,10 @@ export default function createTransitionManager(history, routes) {
180206
const routeID = getRouteID(route)
181207
let hooks = RouteHooks[routeID]
182208

183-
if (hooks == null) {
209+
if (!hooks) {
184210
let thereWereNoRouteHooks = !hasAnyProperties(RouteHooks)
185211

186-
hooks = RouteHooks[routeID] = [ hook ]
212+
RouteHooks[routeID] = [ hook ]
187213

188214
if (thereWereNoRouteHooks) {
189215
// setup transition & beforeunload hooks
@@ -199,24 +225,11 @@ export default function createTransitionManager(history, routes) {
199225
return function () {
200226
const hooks = RouteHooks[routeID]
201227

202-
if (hooks != null) {
228+
if (hooks) {
203229
const newHooks = hooks.filter(item => item !== hook)
204230

205231
if (newHooks.length === 0) {
206-
delete RouteHooks[routeID]
207-
208-
if (!hasAnyProperties(RouteHooks)) {
209-
// teardown transition & beforeunload hooks
210-
if (unlistenBefore) {
211-
unlistenBefore()
212-
unlistenBefore = null
213-
}
214-
215-
if (unlistenBeforeUnload) {
216-
unlistenBeforeUnload()
217-
unlistenBeforeUnload = null
218-
}
219-
}
232+
removeListenBeforeHooksForRoute(route)
220233
} else {
221234
RouteHooks[routeID] = newHooks
222235
}

0 commit comments

Comments
 (0)