Skip to content

Commit a6a50a6

Browse files
committed
feat: cancel pending enter/change hooks on location change
1 parent cd3479b commit a6a50a6

File tree

2 files changed

+107
-7
lines changed

2 files changed

+107
-7
lines changed

modules/TransitionUtils.js

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,49 @@
11
import { loopAsync } from './AsyncUtils'
22

3-
function createTransitionHook(hook, route, asyncArity) {
4-
return function (...args) {
3+
class PendingHooks {
4+
hooks = []
5+
add = hook => this.hooks.push(hook)
6+
remove = hook => this.hooks = this.hooks.filter(h => h !== hook)
7+
has = hook => this.hooks.indexOf(hook) !== -1
8+
clear = () => this.hooks = []
9+
}
10+
11+
const enterHooks = new PendingHooks()
12+
const changeHooks = new PendingHooks()
13+
14+
function createTransitionHook(hook, route, asyncArity, pendingHooks) {
15+
const transitionHook = (...args) => {
516
hook.apply(route, args)
617

718
if (hook.length < asyncArity) {
819
let callback = args[args.length - 1]
20+
// Add synchronous hook to pendingHooks (gets removed instantly later)
21+
pendingHooks.add(transitionHook)
922
// Assume hook executes synchronously and
1023
// automatically call the callback.
1124
callback()
1225
}
1326
}
27+
28+
if (hook.length >= asyncArity) {
29+
pendingHooks.add(transitionHook)
30+
}
31+
32+
return transitionHook
1433
}
1534

1635
function getEnterHooks(routes) {
1736
return routes.reduce(function (hooks, route) {
1837
if (route.onEnter)
19-
hooks.push(createTransitionHook(route.onEnter, route, 3))
20-
38+
hooks.push(createTransitionHook(route.onEnter, route, 3, enterHooks))
2139
return hooks
2240
}, [])
2341
}
2442

2543
function getChangeHooks(routes) {
2644
return routes.reduce(function (hooks, route) {
2745
if (route.onChange)
28-
hooks.push(createTransitionHook(route.onChange, route, 4))
46+
hooks.push(createTransitionHook(route.onChange, route, 4, changeHooks))
2947
return hooks
3048
}, [])
3149
}
@@ -63,9 +81,16 @@ function runTransitionHooks(length, iter, callback) {
6381
* which could lead to a non-responsive UI if the hook is slow.
6482
*/
6583
export function runEnterHooks(routes, nextState, callback) {
84+
enterHooks.clear()
6685
const hooks = getEnterHooks(routes)
6786
return runTransitionHooks(hooks.length, (index, replace, next) => {
68-
hooks[index](nextState, replace, next)
87+
const wrappedNext = () => {
88+
if (enterHooks.has(hooks[index])) {
89+
next()
90+
enterHooks.remove(hooks[index])
91+
}
92+
}
93+
hooks[index](nextState, replace, wrappedNext)
6994
}, callback)
7095
}
7196

@@ -80,9 +105,16 @@ export function runEnterHooks(routes, nextState, callback) {
80105
* which could lead to a non-responsive UI if the hook is slow.
81106
*/
82107
export function runChangeHooks(routes, state, nextState, callback) {
108+
changeHooks.clear()
83109
const hooks = getChangeHooks(routes)
84110
return runTransitionHooks(hooks.length, (index, replace, next) => {
85-
hooks[index](state, nextState, replace, next)
111+
const wrappedNext = () => {
112+
if (changeHooks.has(hooks[index])) {
113+
next()
114+
changeHooks.remove(hooks[index])
115+
}
116+
}
117+
hooks[index](state, nextState, replace, wrappedNext)
86118
}, callback)
87119
}
88120

modules/__tests__/transitionHooks-test.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import createHistory from '../createMemoryHistory'
55
import { routerShape } from '../PropTypes'
66
import execSteps from './execSteps'
77
import Router from '../Router'
8+
import Route from '../Route'
89

910
describe('When a router enters a branch', function () {
1011
let
@@ -374,5 +375,72 @@ describe('When a router enters a branch', function () {
374375
})
375376
})
376377
})
378+
})
379+
380+
describe('Changing location', () => {
381+
let node, onEnterSpy, onChangeSpy
382+
383+
const Text = text => () => <p>{text}</p>
384+
const noop = () => {}
377385

386+
const onEnter = (state, replace, cb) => {
387+
setTimeout(() => {
388+
onEnterSpy()
389+
replace('/bar')
390+
cb()
391+
})
392+
}
393+
const onChange = (prevState, nextState, replace, cb) => {
394+
setTimeout(() => {
395+
onChangeSpy()
396+
replace('/bar')
397+
cb()
398+
})
399+
}
400+
const createRoutes = ({ enter, change }) => [
401+
<Route path="/" onChange={change ? onChange : noop} component={Text('Home')}>
402+
<Route path="child1" component={Text('Child1')} />
403+
<Route path="child2" component={Text('Child2')} />
404+
</Route>,
405+
<Route path="/foo" onEnter={enter ? onEnter : noop} component={Text('Foo')} />,
406+
<Route path="/bar" component={Text('Bar')} />
407+
]
408+
409+
beforeEach(() => {
410+
node = document.createElement('div')
411+
onEnterSpy = expect.createSpy()
412+
onChangeSpy = expect.createSpy()
413+
})
414+
415+
it('cancels pending async onEnter hook', (done) => {
416+
const history = createHistory('/')
417+
const routes = createRoutes({ enter: true })
418+
419+
render(<Router history={history} routes={routes} />, node, () => {
420+
history.push('/foo')
421+
history.push('/')
422+
expect(onEnterSpy.calls.length).toEqual(0)
423+
setTimeout(() => {
424+
expect(onEnterSpy.calls.length).toEqual(1)
425+
expect(node.innerHTML).toContain('Home')
426+
done()
427+
})
428+
})
429+
})
430+
431+
it('cancels pending async onChange hook', (done) => {
432+
const history = createHistory('/')
433+
const routes = createRoutes({ change: true })
434+
435+
render(<Router history={history} routes={routes} />, node, () => {
436+
history.push('/child1')
437+
history.push('/bar')
438+
expect(onChangeSpy.calls.length).toEqual(0)
439+
setTimeout(() => {
440+
expect(onChangeSpy.calls.length).toEqual(1)
441+
expect(node.innerHTML).toContain('Bar')
442+
done()
443+
})
444+
})
445+
})
378446
})

0 commit comments

Comments
 (0)