diff --git a/errors/middleware-upgrade-guide.md b/errors/middleware-upgrade-guide.md index 8c04cc6c4f3b..94af0bceeae5 100644 --- a/errors/middleware-upgrade-guide.md +++ b/errors/middleware-upgrade-guide.md @@ -388,4 +388,4 @@ Prior to Next.js `v12.2`, Middleware was not executed for `_next` requests. For cases where Middleware is used for authorization, you should migrate to use `rewrite`/`redirect` to Pages that show an authorization error, login forms, or to an API Route. -See [No Reponse Body](#no-response-body) for an example of how to migrate to use `rewrite`/`redirect`. +See [No Response Body](#no-response-body) for an example of how to migrate to use `rewrite`/`redirect`. diff --git a/packages/next/README.md b/packages/next/README.md index dd075a57575b..a85ea7acb225 100644 --- a/packages/next/README.md +++ b/packages/next/README.md @@ -1,6 +1,6 @@

- +

Next.js

diff --git a/packages/next/client/script.tsx b/packages/next/client/script.tsx index c1adef2378bd..e9ebd6a022c0 100644 --- a/packages/next/client/script.tsx +++ b/packages/next/client/script.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useContext } from 'react' +import React, { useEffect, useContext, useRef } from 'react' import { ScriptHTMLAttributes } from 'react' import { HeadManagerContext } from '../shared/lib/head-manager-context' import { DOMAttributeNames } from './head-manager' @@ -57,21 +57,25 @@ const loadScript = (props: ScriptProps): void => { return } + /** Execute after the script first loaded */ + const afterLoad = () => { + // Run onReady for the first time after load event + if (onReady) { + onReady() + } + // add cacheKey to LoadCache when load successfully + LoadCache.add(cacheKey) + } + const el = document.createElement('script') const loadPromise = new Promise((resolve, reject) => { el.addEventListener('load', function (e) { - // add cacheKey to LoadCache when load successfully - LoadCache.add(cacheKey) - resolve() if (onLoad) { onLoad.call(this, e) } - // Run onReady for the first time after load event - if (onReady) { - onReady() - } + afterLoad() }) el.addEventListener('error', function (e) { reject(e) @@ -85,8 +89,7 @@ const loadScript = (props: ScriptProps): void => { if (dangerouslySetInnerHTML) { el.innerHTML = dangerouslySetInnerHTML.__html || '' - // add cacheKey to LoadCache for inline script - LoadCache.add(cacheKey) + afterLoad() } else if (children) { el.textContent = typeof children === 'string' @@ -95,8 +98,7 @@ const loadScript = (props: ScriptProps): void => { ? children.join('') : '' - // add cacheKey to LoadCache for inline script - LoadCache.add(cacheKey) + afterLoad() } else if (src) { el.src = src // do not add cacheKey into LoadCache for remote script here @@ -174,12 +176,42 @@ function Script(props: ScriptProps): JSX.Element | null { // Context is available only during SSR const { updateScripts, scripts, getIsSsr } = useContext(HeadManagerContext) + /** + * - First mount: + * 1. The useEffect for onReady executes + * 2. hasOnReadyEffectCalled.current is false, but the script hasn't loaded yet (not in LoadCache) + * onReady is skipped, set hasOnReadyEffectCalled.current to true + * 3. The useEffect for loadScript executes + * Once the script is loaded, the onReady will be called by then + * [If strict mode is enabled / is wrapped in component] + * 5. The useEffect for onReady executes again + * 6. hasOnReadyEffectCalled.current is true, so entire effect is skipped + * 7. The useEffect for loadScript executes again + * 8. The script is already loaded/loading, loadScript bails out + * + * - Second mount: + * 1. The useEffect for onReady executes + * 2. hasOnReadyEffectCalled.current is false, but the script has already loaded (found in LoadCache) + * onReady is called, set hasOnReadyEffectCalled.current to true + * 3. The useEffect for loadScript executes + * 4. The script is already loaded, loadScript bails out + * [If strict mode is enabled / is wrapped in component] + * 5. The useEffect for onReady executes again + * 6. hasOnReadyEffectCalled.current is true, so entire effect is skipped + * 7. The useEffect for loadScript executes again + * 8. The script is already loaded, loadScript will bail out + */ + const hasOnReadyEffectCalled = useRef(false) + useEffect(() => { const cacheKey = id || src + if (!hasOnReadyEffectCalled.current) { + // Run onReady if script has loaded before but component is re-mounted + if (onReady && cacheKey && LoadCache.has(cacheKey)) { + onReady() + } - // Run onReady if script has loaded before but component is re-mounted - if (onReady && cacheKey && LoadCache.has(cacheKey)) { - onReady() + hasOnReadyEffectCalled.current = true } }, [onReady, id, src]) diff --git a/test/integration/script-loader/strictmode/next.config.js b/test/integration/script-loader/base/next.config.js similarity index 100% rename from test/integration/script-loader/strictmode/next.config.js rename to test/integration/script-loader/base/next.config.js diff --git a/test/integration/script-loader/base/pages/page10.js b/test/integration/script-loader/base/pages/page10.js index dd10c2f6a1d1..e2fdde0e776f 100644 --- a/test/integration/script-loader/base/pages/page10.js +++ b/test/integration/script-loader/base/pages/page10.js @@ -1,11 +1,6 @@ import Script from 'next/script' import Link from 'next/link' -if (typeof window !== 'undefined') { - window.remoteScriptsOnReadyCalls ??= 0 - window.inlineScriptsOnReadyCalls ??= 0 -} - const Page = () => { return (
@@ -14,6 +9,7 @@ const Page = () => {