From 5f41abda9ac71923584446a60dafa5601aa3bfe1 Mon Sep 17 00:00:00 2001 From: Joe Haddad Date: Thu, 11 Feb 2021 13:51:41 -0500 Subject: [PATCH] fix(link): cancel idle callback on unmount (#22072) Co-authored-by: mAAdhaTTah --- package.json | 1 + packages/next/client/experimental-script.tsx | 2 +- packages/next/client/request-idle-callback.ts | 9 +- packages/next/client/route-loader.ts | 2 +- packages/next/client/use-intersection.tsx | 10 +- packages/next/compiled/strip-ansi/index.js | 2 +- .../build-output/test/index.test.js | 6 +- .../integration/size-limit/test/index.test.js | 2 +- test/unit/link-warnings.test.js | 33 +++++++ yarn.lock | 93 +++++++++++++++++++ 10 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 test/unit/link-warnings.test.js diff --git a/package.json b/package.json index 8a001623e1..d52397c3fa 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@opentelemetry/plugin-http": "0.14.0", "@opentelemetry/plugin-https": "0.14.0", "@opentelemetry/tracing": "0.14.0", + "@testing-library/react": "11.2.5", "@types/cheerio": "0.22.16", "@types/fs-extra": "8.1.0", "@types/http-proxy": "1.17.3", diff --git a/packages/next/client/experimental-script.tsx b/packages/next/client/experimental-script.tsx index d5b645a58a..04f3cd382c 100644 --- a/packages/next/client/experimental-script.tsx +++ b/packages/next/client/experimental-script.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useContext } from 'react' import { ScriptHTMLAttributes } from 'react' import { HeadManagerContext } from '../next-server/lib/head-manager-context' import { DOMAttributeNames } from './head-manager' -import requestIdleCallback from './request-idle-callback' +import { requestIdleCallback } from './request-idle-callback' const ScriptCache = new Map() const LoadCache = new Set() diff --git a/packages/next/client/request-idle-callback.ts b/packages/next/client/request-idle-callback.ts index 4612765767..a713fcd067 100644 --- a/packages/next/client/request-idle-callback.ts +++ b/packages/next/client/request-idle-callback.ts @@ -13,10 +13,11 @@ declare global { callback: (deadline: RequestIdleCallbackDeadline) => void, opts?: RequestIdleCallbackOptions ) => RequestIdleCallbackHandle + cancelIdleCallback: (id: RequestIdleCallbackHandle) => void } } -const requestIdleCallback = +export const requestIdleCallback = (typeof self !== 'undefined' && self.requestIdleCallback) || function ( cb: (deadline: RequestIdleCallbackDeadline) => void @@ -32,4 +33,8 @@ const requestIdleCallback = }, 1) } -export default requestIdleCallback +export const cancelIdleCallback = + (typeof self !== 'undefined' && self.cancelIdleCallback) || + function (id: RequestIdleCallbackHandle) { + return clearTimeout(id) + } diff --git a/packages/next/client/route-loader.ts b/packages/next/client/route-loader.ts index ba6023cf70..c761dd0912 100644 --- a/packages/next/client/route-loader.ts +++ b/packages/next/client/route-loader.ts @@ -1,7 +1,7 @@ import { ComponentType } from 'react' import { ClientBuildManifest } from '../build/webpack/plugins/build-manifest-plugin' import getAssetPathFromRoute from '../next-server/lib/router/utils/get-asset-path-from-route' -import requestIdleCallback from './request-idle-callback' +import { requestIdleCallback } from './request-idle-callback' // 3.8s was arbitrarily chosen as it's what https://web.dev/interactive // considers as "Good" time-to-interactive. We must assume something went diff --git a/packages/next/client/use-intersection.tsx b/packages/next/client/use-intersection.tsx index ad198e4d7f..62d188f579 100644 --- a/packages/next/client/use-intersection.tsx +++ b/packages/next/client/use-intersection.tsx @@ -1,5 +1,8 @@ import { useCallback, useEffect, useRef, useState } from 'react' -import requestIdleCallback from './request-idle-callback' +import { + requestIdleCallback, + cancelIdleCallback, +} from './request-idle-callback' type UseIntersectionObserverInit = Pick type UseIntersection = { disabled?: boolean } & UseIntersectionObserverInit @@ -43,7 +46,10 @@ export function useIntersection({ useEffect(() => { if (!hasIntersectionObserver) { - if (!visible) requestIdleCallback(() => setVisible(true)) + if (!visible) { + const idleCallback = requestIdleCallback(() => setVisible(true)) + return () => cancelIdleCallback(idleCallback) + } } }, [visible]) diff --git a/packages/next/compiled/strip-ansi/index.js b/packages/next/compiled/strip-ansi/index.js index cfe50667e6..cd19d931ef 100644 --- a/packages/next/compiled/strip-ansi/index.js +++ b/packages/next/compiled/strip-ansi/index.js @@ -1 +1 @@ -module.exports=(()=>{"use strict";var e={301:(e,r,t)=>{const _=t(979);e.exports=(e=>typeof e==="string"?e.replace(_(),""):e)},979:e=>{e.exports=(({onlyFirst:e=false}={})=>{const r=["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)","(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"].join("|");return new RegExp(r,e?undefined:"g")})}};var r={};function __nccwpck_require__(t){if(r[t]){return r[t].exports}var _=r[t]={exports:{}};var n=true;try{e[t](_,_.exports,__nccwpck_require__);n=false}finally{if(n)delete r[t]}return _.exports}__nccwpck_require__.ab=__dirname+"/";return __nccwpck_require__(301)})(); \ No newline at end of file +module.exports=(()=>{"use strict";var e={161:e=>{e.exports=(({onlyFirst:e=false}={})=>{const r=["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)","(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"].join("|");return new RegExp(r,e?undefined:"g")})},301:(e,r,t)=>{const _=t(161);e.exports=(e=>typeof e==="string"?e.replace(_(),""):e)}};var r={};function __nccwpck_require__(t){if(r[t]){return r[t].exports}var _=r[t]={exports:{}};var n=true;try{e[t](_,_.exports,__nccwpck_require__);n=false}finally{if(n)delete r[t]}return _.exports}__nccwpck_require__.ab=__dirname+"/";return __nccwpck_require__(301)})(); \ No newline at end of file diff --git a/test/integration/build-output/test/index.test.js b/test/integration/build-output/test/index.test.js index 468cc1fc6b..d693b3162e 100644 --- a/test/integration/build-output/test/index.test.js +++ b/test/integration/build-output/test/index.test.js @@ -94,14 +94,14 @@ describe('Build Output', () => { expect(parseFloat(indexSize) - 266).toBeLessThanOrEqual(0) expect(indexSize.endsWith('B')).toBe(true) - // should be no bigger than 63.8 kb - expect(parseFloat(indexFirstLoad)).toBeCloseTo(63.8, 1) + // should be no bigger than 63.9 kb + expect(parseFloat(indexFirstLoad)).toBeCloseTo(63.9, 1) expect(indexFirstLoad.endsWith('kB')).toBe(true) expect(parseFloat(err404Size) - 3.7).toBeLessThanOrEqual(0) expect(err404Size.endsWith('kB')).toBe(true) - expect(parseFloat(err404FirstLoad)).toBeCloseTo(67, 1) + expect(parseFloat(err404FirstLoad)).toBeCloseTo(67.1, 0) expect(err404FirstLoad.endsWith('kB')).toBe(true) expect(parseFloat(sharedByAll)).toBeCloseTo(63.6, 1) diff --git a/test/integration/size-limit/test/index.test.js b/test/integration/size-limit/test/index.test.js index a30cdafa66..930c12a0dc 100644 --- a/test/integration/size-limit/test/index.test.js +++ b/test/integration/size-limit/test/index.test.js @@ -81,6 +81,6 @@ describe('Production response size', () => { const delta = responseSizesBytes / 1024 // Expected difference: < 0.5 - expect(delta).toBeCloseTo(284.1, 0) + expect(delta).toBeCloseTo(284.7, 0) }) }) diff --git a/test/unit/link-warnings.test.js b/test/unit/link-warnings.test.js new file mode 100644 index 0000000000..0db43b5ff6 --- /dev/null +++ b/test/unit/link-warnings.test.js @@ -0,0 +1,33 @@ +/** + * @jest-environment jsdom + */ +import { act, render } from '@testing-library/react' +import Link from 'next/link' + +describe('', () => { + let spy + beforeAll(() => { + spy = jest.spyOn(console, 'error').mockImplementation(() => {}) + }) + + it('test link with unmount', () => { + act(() => { + const { unmount } = render(hello) + unmount() + }) + + expect(spy).not.toHaveBeenCalled() + }) + + it('test link without unmount', () => { + act(() => { + render(hello) + }) + + expect(spy).not.toHaveBeenCalled() + }) + + afterAll(() => { + spy.mockRestore() + }) +}) diff --git a/yarn.lock b/yarn.lock index 1f8e90fe3f..a935236467 100644 --- a/yarn.lock +++ b/yarn.lock @@ -958,6 +958,14 @@ pirates "^4.0.0" source-map-support "^0.5.16" +"@babel/runtime-corejs3@^7.10.2": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.12.13.tgz#53d09813b7c20d616caf258e9325550ff701c039" + integrity sha512-8fSpqYRETHATtNitsCXq8QQbKJP31/KnDl2Wz2Vtui9nKzjss2ysuZtyVsWjBtvkeEFo346gkwjYPab1hvrXkQ== + dependencies: + core-js-pure "^3.0.0" + regenerator-runtime "^0.13.4" + "@babel/runtime-corejs3@^7.12.1": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz#ffee91da0eb4c6dae080774e94ba606368e414f4" @@ -973,6 +981,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.10.2": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.13.tgz#0a21452352b02542db0ffb928ac2d3ca7cb6d66d" + integrity sha512-8+3UMPBrjFa/6TtKi/7sehPKqfAm4g6K+YQjyyFOLUTxzOngcRZTlAVY8sc2CORJYqdHQY8gRPHmn+qo15rCBw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.3.3", "@babel/template@^7.4.0": version "7.12.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc" @@ -1551,6 +1566,17 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@jest/types@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + "@lerna/add@3.14.0": version "3.14.0" resolved "https://registry.yarnpkg.com/@lerna/add/-/add-3.14.0.tgz#799d416e67d48c285967abf883be746557aefa48" @@ -2604,12 +2630,39 @@ dependencies: chokidar "^1.7.0" +"@testing-library/dom@^7.28.1": + version "7.29.4" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-7.29.4.tgz#1647c2b478789621ead7a50614ad81ab5ae5b86c" + integrity sha512-CtrJRiSYEfbtNGtEsd78mk1n1v2TUbeABlNIcOCJdDfkN5/JTOwQEbbQpoSRxGqzcWPgStMvJ4mNolSuBRv1NA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^4.2.2" + chalk "^4.1.0" + dom-accessibility-api "^0.5.4" + lz-string "^1.4.4" + pretty-format "^26.6.2" + +"@testing-library/react@11.2.5": + version "11.2.5" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-11.2.5.tgz#ae1c36a66c7790ddb6662c416c27863d87818eb9" + integrity sha512-yEx7oIa/UWLe2F2dqK0FtMF9sJWNXD+2PPtp39BvE0Kh9MJ9Kl0HrZAgEuhUJR+Lx8Di6Xz+rKwSdEPY2UV8ZQ== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^7.28.1" + "@types/amphtml-validator@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/amphtml-validator/-/amphtml-validator-1.0.0.tgz#9d4e0c879642938bbe5f363d49cafc8ae9f57c81" dependencies: "@types/node" "*" +"@types/aria-query@^4.2.0": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.1.tgz#78b5433344e2f92e8b306c06a5622c50c245bf6b" + integrity sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg== + "@types/async-retry@1.4.2": version "1.4.2" resolved "https://registry.yarnpkg.com/@types/async-retry/-/async-retry-1.4.2.tgz#7f910188cd3893b51e32df51765ee8d5646053e3" @@ -2811,6 +2864,13 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" +"@types/istanbul-reports@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz#508b13aa344fa4976234e75dddcc34925737d821" + integrity sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA== + dependencies: + "@types/istanbul-lib-report" "*" + "@types/jest-diff@*": version "24.3.0" resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-24.3.0.tgz#29e237a3d954babfe6e23cc59b57ecd8ca8d858d" @@ -3578,6 +3638,14 @@ args@4.0.0: leven "2.1.0" mri "1.1.0" +aria-query@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" + integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== + dependencies: + "@babel/runtime" "^7.10.2" + "@babel/runtime-corejs3" "^7.10.2" + arity-n@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/arity-n/-/arity-n-1.0.4.tgz#d9e76b11733e08569c0847ae7b39b2860b30b745" @@ -6072,6 +6140,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz#b06d059cdd4a4ad9a79275f9d414a5c126241166" + integrity sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ== + dom-serializer@0: version "0.2.2" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" @@ -10244,6 +10317,11 @@ lru_map@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= + macos-release@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f" @@ -12943,6 +13021,16 @@ pretty-format@^26.0.1: ansi-styles "^4.0.0" react-is "^16.12.0" +pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== + dependencies: + "@jest/types" "^26.6.2" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^17.0.1" + pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" @@ -13248,6 +13336,11 @@ react-is@16.13.1, react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1, react-i version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" +react-is@^17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" + integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== + react-refresh@0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"