diff --git a/docs/api-reference/next/image.md b/docs/api-reference/next/image.md
index 17687eb93a201..830771ce34471 100644
--- a/docs/api-reference/next/image.md
+++ b/docs/api-reference/next/image.md
@@ -16,6 +16,7 @@ description: Enable Image Optimization with the built-in Image component.
| Version | Changes |
| --------- | ------------------------------------------------------------------------------------------------- |
+| `v12.0.9` | `lazyRoot` prop added |
| `v12.0.0` | `formats` configuration added.
AVIF support added.
Wrapper `
` changed to `
`. |
| `v11.1.0` | `onLoadingComplete` and `lazyBoundary` props added. |
| `v11.0.0` | `src` prop support for static import.
`placeholder` prop added.
`blurDataURL` prop added. |
@@ -221,6 +222,12 @@ A string (with similar syntax to the margin property) that acts as the bounding
[Learn more](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin)
+### lazyRoot
+
+A React [Ref](https://reactjs.org/docs/refs-and-the-dom.html) pointing to the Element which the [lazyBoundary](#lazyBoundary) calculates for the Intersection detection. Defaults to `null`, referring to the document viewport.
+
+[Learn more](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/root)
+
### unoptimized
When true, the source image will be served as-is instead of changing quality,
diff --git a/packages/next/client/image.tsx b/packages/next/client/image.tsx
index 04f2f9a175fdd..0980f8abd535b 100644
--- a/packages/next/client/image.tsx
+++ b/packages/next/client/image.tsx
@@ -100,6 +100,7 @@ export type ImageProps = Omit<
quality?: number | string
priority?: boolean
loading?: LoadingValue
+ lazyRoot?: React.RefObject | null
lazyBoundary?: string
placeholder?: PlaceholderValue
blurDataURL?: string
@@ -319,6 +320,7 @@ export default function Image({
unoptimized = false,
priority = false,
loading,
+ lazyRoot = null,
lazyBoundary = '200px',
className,
quality,
@@ -510,6 +512,7 @@ export default function Image({
}
const [setIntersection, isIntersected] = useIntersection({
+ rootRef: lazyRoot,
rootMargin: lazyBoundary,
disabled: !isLazy,
})
diff --git a/packages/next/client/use-intersection.tsx b/packages/next/client/use-intersection.tsx
index 62d188f579a1d..94ee9d97f1d1e 100644
--- a/packages/next/client/use-intersection.tsx
+++ b/packages/next/client/use-intersection.tsx
@@ -4,8 +4,14 @@ import {
cancelIdleCallback,
} from './request-idle-callback'
-type UseIntersectionObserverInit = Pick
-type UseIntersection = { disabled?: boolean } & UseIntersectionObserverInit
+type UseIntersectionObserverInit = Pick<
+ IntersectionObserverInit,
+ 'rootMargin' | 'root'
+>
+
+type UseIntersection = { disabled?: boolean } & UseIntersectionObserverInit & {
+ rootRef?: React.RefObject | null
+ }
type ObserveCallback = (isVisible: boolean) => void
type Observer = {
id: string
@@ -16,6 +22,7 @@ type Observer = {
const hasIntersectionObserver = typeof IntersectionObserver !== 'undefined'
export function useIntersection({
+ rootRef,
rootMargin,
disabled,
}: UseIntersection): [(element: T | null) => void, boolean] {
@@ -23,7 +30,7 @@ export function useIntersection({
const unobserve = useRef()
const [visible, setVisible] = useState(false)
-
+ const [root, setRoot] = useState(rootRef ? rootRef.current : null)
const setRef = useCallback(
(el: T | null) => {
if (unobserve.current) {
@@ -37,11 +44,11 @@ export function useIntersection({
unobserve.current = observe(
el,
(isVisible) => isVisible && setVisible(isVisible),
- { rootMargin }
+ { root, rootMargin }
)
}
},
- [isDisabled, rootMargin, visible]
+ [isDisabled, root, rootMargin, visible]
)
useEffect(() => {
@@ -53,6 +60,9 @@ export function useIntersection({
}
}, [visible])
+ useEffect(() => {
+ if (rootRef) setRoot(rootRef.current)
+ }, [rootRef])
return [setRef, visible]
}
diff --git a/test/integration/image-component/default/pages/lazy-noref.js b/test/integration/image-component/default/pages/lazy-noref.js
new file mode 100644
index 0000000000000..3edfe7df79028
--- /dev/null
+++ b/test/integration/image-component/default/pages/lazy-noref.js
@@ -0,0 +1,32 @@
+import React, { useRef } from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ const myRef = useRef(null)
+
+ return (
+ <>
+
+ >
+ )
+}
+export default Page
diff --git a/test/integration/image-component/default/pages/lazy-withref.js b/test/integration/image-component/default/pages/lazy-withref.js
new file mode 100644
index 0000000000000..d21a4c0a9dad4
--- /dev/null
+++ b/test/integration/image-component/default/pages/lazy-withref.js
@@ -0,0 +1,33 @@
+import React, { useRef } from 'react'
+import Image from 'next/image'
+
+const Page = () => {
+ const myRef = useRef(null)
+
+ return (
+ <>
+
+ >
+ )
+}
+export default Page
diff --git a/test/integration/image-component/default/test/index.test.js b/test/integration/image-component/default/test/index.test.js
index d7d2c050f7c49..5dae3d31658c8 100644
--- a/test/integration/image-component/default/test/index.test.js
+++ b/test/integration/image-component/default/test/index.test.js
@@ -1008,6 +1008,37 @@ function runTests(mode) {
}
}
})
+
+ it('should load the image when the lazyRoot prop is used', async () => {
+ let browser
+ try {
+ //trying on '/lazy-noref' it fails
+ browser = await webdriver(appPort, '/lazy-withref')
+
+ await check(async () => {
+ const result = await browser.eval(
+ `document.getElementById('myImage').naturalWidth`
+ )
+
+ if (result < 400) {
+ throw new Error('Incorrectly loaded image')
+ }
+
+ return 'result-correct'
+ }, /result-correct/)
+
+ expect(
+ await hasImageMatchingUrl(
+ browser,
+ `http://localhost:${appPort}/_next/image?url=%2Ftest.jpg&w=828&q=75`
+ )
+ ).toBe(true)
+ } finally {
+ if (browser) {
+ await browser.close()
+ }
+ }
+ })
}
describe('Image Component Tests', () => {