Skip to content

Commit

Permalink
adds onReady prop to next/script
Browse files Browse the repository at this point in the history
  • Loading branch information
housseindjirdeh committed Jul 20, 2022
1 parent 20486c1 commit dd6fda1
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 5 deletions.
35 changes: 33 additions & 2 deletions docs/api-reference/next/script.md
Expand Up @@ -20,6 +20,7 @@ description: Optimize loading of third-party scripts with the built-in Script co

| Version | Changes |
| --------- | ------------------------- |
| `v12.2.3` | `onReady` prop added. |
| `v11.0.0` | `next/script` introduced. |

</details>
Expand Down Expand Up @@ -47,9 +48,9 @@ The loading strategy of the script.
### onLoad

A method that returns additional JavaScript that should be executed after the script has finished loading.
A method that returns additional JavaScript that should be executed once after the script has finished loading.

> **Note: `onLoad` can't be used with the `beforeInteractive` loading strategy.**
> **Note: `onLoad` can't be used with the `beforeInteractive` loading strategy. Consider using `onReady` instead.**
The following is an example of how to use the `onLoad` property:

Expand All @@ -74,6 +75,36 @@ export default function Home() {
}
```

### onReady

A method that returns additional JavaScript that should be executed after the script has finished loading and every time the component is mounted.

The following is an example of how to use the `onReady` property:

```jsx
import { useState } from 'react'
import Script from 'next/script'

export default function Home() {
const [stripe, setStripe] = useState(null)

return (
<>
<Script
id="google-maps"
src="https://maps.googleapis.com/maps/api/js"
onReady={() => {
new google.maps.Map(ref.current, {
center: { lat: -34.397, lng: 150.644 },
zoom: 8,
})
}}
/>
</>
)
}
```

### onError

A method that executes if the script fails to load.
Expand Down
34 changes: 32 additions & 2 deletions docs/basic-features/script.md
Expand Up @@ -21,6 +21,7 @@ description: Next.js helps you optimize loading third-party scripts with the bui

| Version | Changes |
| --------- | ------------------------- |
| `v12.2.3` | `onReady` prop added. |
| `v11.0.0` | `next/script` introduced. |

</details>
Expand Down Expand Up @@ -249,9 +250,9 @@ The `id` property is required for **inline scripts** in order for Next.js to tra

### Executing Code After Loading (`onLoad`)

> **Note: `onLoad` and `onError` cannot be used with the `beforeInteractive` loading strategy.**
> **Note: `onLoad` cannot be used with the `beforeInteractive` loading strategy. Consider using `onReady` instead.**
Some third-party scripts require users to run JavaScript code after the script has finished loading in order to instantiate content or call a function. If you are loading a script with either `afterInteractive` or `lazyOnload` as a loading strategy, you can execute code after it has loaded using the `onLoad` property:
Some third-party scripts require users to run JavaScript code once after the script has finished loading in order to instantiate content or call a function. If you are loading a script with either `afterInteractive` or `lazyOnload` as a loading strategy, you can execute code after it has loaded using the `onLoad` property:

```jsx
import { useState } from 'react'
Expand All @@ -274,6 +275,35 @@ export default function Home() {
}
```

### Executing Code After Mounting (`onReady`)

Some third-party scripts require users to run JavaScript code after the script has finished loading and every time the component is mounted (after a route navigation for example). You can execute code after the script's `load` event when it first loads and then after every subsequent component re-mount using the `onReady` property:

```jsx
import Script from 'next/script'

export default function Home() {
return (
<>
<Script
id="google-maps"
src="https://maps.googleapis.com/maps/api/js"
onReady={() => {
new google.maps.Map(ref.current, {
center: { lat: -34.397, lng: 150.644 },
zoom: 8,
})
}}
/>
</>
)
}
```

### Handling errors (`onError`)

> **Note: `onError` cannot be used with the `beforeInteractive` loading strategy.**
Sometimes it is helpful to catch when a script fails to load. These errors can be handled with the `onError` property:

```jsx
Expand Down
22 changes: 21 additions & 1 deletion packages/next/client/script.tsx
Expand Up @@ -11,6 +11,7 @@ export interface ScriptProps extends ScriptHTMLAttributes<HTMLScriptElement> {
strategy?: 'afterInteractive' | 'lazyOnload' | 'beforeInteractive' | 'worker'
id?: string
onLoad?: (e: any) => void
onReady?: () => void | null
onError?: (e: any) => void
children?: React.ReactNode
}
Expand All @@ -22,6 +23,7 @@ export type Props = ScriptProps

const ignoreProps = [
'onLoad',
'onReady',
'dangerouslySetInnerHTML',
'children',
'onError',
Expand All @@ -33,6 +35,7 @@ const loadScript = (props: ScriptProps): void => {
src,
id,
onLoad = () => {},
onReady = null,
dangerouslySetInnerHTML,
children = '',
strategy = 'afterInteractive',
Expand Down Expand Up @@ -62,6 +65,10 @@ const loadScript = (props: ScriptProps): void => {
if (onLoad) {
onLoad.call(this, e)
}
// Run onReady for the first time after load event
if (onReady) {
onReady()
}
})
el.addEventListener('error', function (e) {
reject(e)
Expand Down Expand Up @@ -147,8 +154,10 @@ export function initScriptLoader(scriptLoaderItems: ScriptProps[]) {

function Script(props: ScriptProps): JSX.Element | null {
const {
id,
src = '',
onLoad = () => {},
onReady = null,
strategy = 'afterInteractive',
onError,
...restProps
Expand All @@ -157,6 +166,15 @@ function Script(props: ScriptProps): JSX.Element | null {
// Context is available only during SSR
const { updateScripts, scripts, getIsSsr } = useContext(HeadManagerContext)

useEffect(() => {
const cacheKey = id || src

// Run onReady if script has loaded before but component is re-mounted
if (onReady && cacheKey && LoadCache.has(cacheKey)) {
onReady()
}
}, [onReady, id, src])

useEffect(() => {
if (strategy === 'afterInteractive') {
loadScript(props)
Expand All @@ -169,16 +187,18 @@ function Script(props: ScriptProps): JSX.Element | null {
if (updateScripts) {
scripts[strategy] = (scripts[strategy] || []).concat([
{
id,
src,
onLoad,
onReady,
onError,
...restProps,
},
])
updateScripts(scripts)
} else if (getIsSsr && getIsSsr()) {
// Script has already loaded during SSR
LoadCache.add(restProps.id || src)
LoadCache.add(id || src)
} else if (getIsSsr && !getIsSsr()) {
loadScript(props)
}
Expand Down
24 changes: 24 additions & 0 deletions test/integration/script-loader/base/pages/page8.js
@@ -0,0 +1,24 @@
import Script from 'next/script'
import Link from 'next/link'

const url =
'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js'

const Page = () => {
return (
<div class="container">
<Link href="/page9">Page 9</Link>
<div id="text"></div>
<Script
src={url}
id="script1"
onReady={() => {
// eslint-disable-next-line no-undef
document.getElementById('text').textContent += _.repeat('a', 3)
}}
></Script>
</div>
)
}

export default Page
11 changes: 11 additions & 0 deletions test/integration/script-loader/base/pages/page9.js
@@ -0,0 +1,11 @@
import Link from 'next/link'

const Page = () => {
return (
<>
<Link href="/page8">Page 8</Link>
</>
)
}

export default Page
24 changes: 24 additions & 0 deletions test/integration/script-loader/test/index.test.js
Expand Up @@ -190,6 +190,30 @@ describe('Next.js Script - Primary Strategies', () => {
}
})

it('onReady fires after load event and then on every subsequent re-mount', async () => {
let browser
try {
browser = await webdriver(appPort, '/page8')

const text = await browser.elementById('text').text()

expect(text).toBe('aaa')

// Navigate to different page and back
await browser.waitForElementByCss('[href="/page9"]')
await browser.click('[href="/page9"]')
await browser.waitForElementByCss('[href="/page8"]')
await browser.click('[href="/page8"]')

await browser.waitForElementByCss('.container')
const sameText = await browser.elementById('text').text()

expect(sameText).toBe('aaa') // onReady should fire again
} finally {
if (browser) await browser.close()
}
})

it('priority beforeInteractive with inline script', async () => {
const html = await renderViaHTTP(appPort, '/page5')
const $ = cheerio.load(html)
Expand Down

0 comments on commit dd6fda1

Please sign in to comment.