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 = () => {