From 684248e3aa681daace1e6a2e8cad505315e08def Mon Sep 17 00:00:00 2001 From: Ali Sheehan-Dare Date: Tue, 25 Oct 2016 01:07:35 +0100 Subject: [PATCH] feat: cancel pending enter/change hooks on location change (#4063) * feat: cancel pending enter/change hooks on location change * chore: fix npm build scripts on windows --- modules/TransitionUtils.js | 46 ++++++++++++--- modules/__tests__/transitionHooks-test.js | 68 +++++++++++++++++++++++ package.json | 4 +- 3 files changed, 108 insertions(+), 10 deletions(-) diff --git a/modules/TransitionUtils.js b/modules/TransitionUtils.js index baff7cde63..807e316544 100644 --- a/modules/TransitionUtils.js +++ b/modules/TransitionUtils.js @@ -1,23 +1,39 @@ import { loopAsync } from './AsyncUtils' -function createTransitionHook(hook, route, asyncArity) { - return function (...args) { +class PendingHooks { + hooks = [] + add = hook => this.hooks.push(hook) + remove = hook => this.hooks = this.hooks.filter(h => h !== hook) + has = hook => this.hooks.indexOf(hook) !== -1 + clear = () => this.hooks = [] +} + +const enterHooks = new PendingHooks() +const changeHooks = new PendingHooks() + +function createTransitionHook(hook, route, asyncArity, pendingHooks) { + const isSync = hook.length < asyncArity + + const transitionHook = (...args) => { hook.apply(route, args) - if (hook.length < asyncArity) { + if (isSync) { let callback = args[args.length - 1] // Assume hook executes synchronously and // automatically call the callback. callback() } } + + pendingHooks.add(transitionHook) + + return transitionHook } function getEnterHooks(routes) { return routes.reduce(function (hooks, route) { if (route.onEnter) - hooks.push(createTransitionHook(route.onEnter, route, 3)) - + hooks.push(createTransitionHook(route.onEnter, route, 3, enterHooks)) return hooks }, []) } @@ -25,7 +41,7 @@ function getEnterHooks(routes) { function getChangeHooks(routes) { return routes.reduce(function (hooks, route) { if (route.onChange) - hooks.push(createTransitionHook(route.onChange, route, 4)) + hooks.push(createTransitionHook(route.onChange, route, 4, changeHooks)) return hooks }, []) } @@ -63,9 +79,16 @@ function runTransitionHooks(length, iter, callback) { * which could lead to a non-responsive UI if the hook is slow. */ export function runEnterHooks(routes, nextState, callback) { + enterHooks.clear() const hooks = getEnterHooks(routes) return runTransitionHooks(hooks.length, (index, replace, next) => { - hooks[index](nextState, replace, next) + const wrappedNext = () => { + if (enterHooks.has(hooks[index])) { + next() + enterHooks.remove(hooks[index]) + } + } + hooks[index](nextState, replace, wrappedNext) }, callback) } @@ -80,9 +103,16 @@ export function runEnterHooks(routes, nextState, callback) { * which could lead to a non-responsive UI if the hook is slow. */ export function runChangeHooks(routes, state, nextState, callback) { + changeHooks.clear() const hooks = getChangeHooks(routes) return runTransitionHooks(hooks.length, (index, replace, next) => { - hooks[index](state, nextState, replace, next) + const wrappedNext = () => { + if (changeHooks.has(hooks[index])) { + next() + changeHooks.remove(hooks[index]) + } + } + hooks[index](state, nextState, replace, wrappedNext) }, callback) } diff --git a/modules/__tests__/transitionHooks-test.js b/modules/__tests__/transitionHooks-test.js index 586428a5f1..e77575d9b5 100644 --- a/modules/__tests__/transitionHooks-test.js +++ b/modules/__tests__/transitionHooks-test.js @@ -5,6 +5,7 @@ import createHistory from '../createMemoryHistory' import { routerShape } from '../PropTypes' import execSteps from './execSteps' import Router from '../Router' +import Route from '../Route' describe('When a router enters a branch', function () { let @@ -374,5 +375,72 @@ describe('When a router enters a branch', function () { }) }) }) +}) + +describe('Changing location', () => { + let node, onEnterSpy, onChangeSpy + + const Text = text => () =>

{text}

+ const noop = () => {} + const onEnter = (state, replace, cb) => { + setTimeout(() => { + onEnterSpy() + replace('/bar') + cb() + }) + } + const onChange = (prevState, nextState, replace, cb) => { + setTimeout(() => { + onChangeSpy() + replace('/bar') + cb() + }) + } + const createRoutes = ({ enter, change }) => [ + + + + , + , + + ] + + beforeEach(() => { + node = document.createElement('div') + onEnterSpy = expect.createSpy() + onChangeSpy = expect.createSpy() + }) + + it('cancels pending async onEnter hook', (done) => { + const history = createHistory('/') + const routes = createRoutes({ enter: true }) + + render(, node, () => { + history.push('/foo') + history.push('/') + expect(onEnterSpy.calls.length).toEqual(0) + setTimeout(() => { + expect(onEnterSpy.calls.length).toEqual(1) + expect(node.innerHTML).toContain('Home') + done() + }) + }) + }) + + it('cancels pending async onChange hook', (done) => { + const history = createHistory('/') + const routes = createRoutes({ change: true }) + + render(, node, () => { + history.push('/child1') + history.push('/bar') + expect(onChangeSpy.calls.length).toEqual(0) + setTimeout(() => { + expect(onChangeSpy.calls.length).toEqual(1) + expect(node.innerHTML).toContain('Bar') + done() + }) + }) + }) }) diff --git a/package.json b/package.json index 4ef1c3798d..9981350c30 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "bugs": "https://github.com/ReactTraining/react-router/issues", "scripts": { "build": "npm run build-cjs && npm run build-es", - "build-cjs": "rimraf lib && cross-env BABEL_ENV=cjs babel ./modules -d lib --ignore '__tests__'", - "build-es": "rimraf es && cross-env BABEL_ENV=es babel ./modules -d es --ignore '__tests__'", + "build-cjs": "rimraf lib && cross-env BABEL_ENV=cjs babel ./modules -d lib --ignore __tests__", + "build-es": "rimraf es && cross-env BABEL_ENV=es babel ./modules -d es --ignore __tests__", "build-umd": "cross-env NODE_ENV=development webpack modules/index.js umd/ReactRouter.js", "build-min": "cross-env NODE_ENV=production webpack -p modules/index.js umd/ReactRouter.min.js", "lint": "eslint examples modules scripts tools *.js",