Skip to content

Commit 7452c0b

Browse files
11koukoustyfle
andauthoredJan 25, 2022
Add lazyRoot optional property to next/image component (#33290)
* Added 'rootEl' oprional property to next/Image component resembling 'root' option of the Intersection Observer API * changed 'rootEl' to 'lazyBoundary' and its type as well * added test, fixed initial root detection * Update test/integration/image-component/default/test/index.test.js Co-authored-by: Steven <steven@ceriously.com> * prop names changed * added 'lazyroot' prop to the documentation * removed unused import * Apply suggestions from code review * Update docs with lazyRoot added in 12.0.9 Co-authored-by: Steven <steven@ceriously.com>
1 parent 9dd0399 commit 7452c0b

File tree

6 files changed

+121
-5
lines changed

6 files changed

+121
-5
lines changed
 

‎docs/api-reference/next/image.md

+7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ description: Enable Image Optimization with the built-in Image component.
1616

1717
| Version | Changes |
1818
| --------- | ------------------------------------------------------------------------------------------------- |
19+
| `v12.0.9` | `lazyRoot` prop added |
1920
| `v12.0.0` | `formats` configuration added.<br/>AVIF support added.<br/>Wrapper `<div>` changed to `<span>`. |
2021
| `v11.1.0` | `onLoadingComplete` and `lazyBoundary` props added. |
2122
| `v11.0.0` | `src` prop support for static import.<br/>`placeholder` prop added.<br/>`blurDataURL` prop added. |
@@ -221,6 +222,12 @@ A string (with similar syntax to the margin property) that acts as the bounding
221222

222223
[Learn more](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin)
223224

225+
### lazyRoot
226+
227+
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.
228+
229+
[Learn more](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/root)
230+
224231
### unoptimized
225232

226233
When true, the source image will be served as-is instead of changing quality,

‎packages/next/client/image.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export type ImageProps = Omit<
100100
quality?: number | string
101101
priority?: boolean
102102
loading?: LoadingValue
103+
lazyRoot?: React.RefObject<HTMLElement> | null
103104
lazyBoundary?: string
104105
placeholder?: PlaceholderValue
105106
blurDataURL?: string
@@ -319,6 +320,7 @@ export default function Image({
319320
unoptimized = false,
320321
priority = false,
321322
loading,
323+
lazyRoot = null,
322324
lazyBoundary = '200px',
323325
className,
324326
quality,
@@ -510,6 +512,7 @@ export default function Image({
510512
}
511513

512514
const [setIntersection, isIntersected] = useIntersection<HTMLImageElement>({
515+
rootRef: lazyRoot,
513516
rootMargin: lazyBoundary,
514517
disabled: !isLazy,
515518
})

‎packages/next/client/use-intersection.tsx

+15-5
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ import {
44
cancelIdleCallback,
55
} from './request-idle-callback'
66

7-
type UseIntersectionObserverInit = Pick<IntersectionObserverInit, 'rootMargin'>
8-
type UseIntersection = { disabled?: boolean } & UseIntersectionObserverInit
7+
type UseIntersectionObserverInit = Pick<
8+
IntersectionObserverInit,
9+
'rootMargin' | 'root'
10+
>
11+
12+
type UseIntersection = { disabled?: boolean } & UseIntersectionObserverInit & {
13+
rootRef?: React.RefObject<HTMLElement> | null
14+
}
915
type ObserveCallback = (isVisible: boolean) => void
1016
type Observer = {
1117
id: string
@@ -16,14 +22,15 @@ type Observer = {
1622
const hasIntersectionObserver = typeof IntersectionObserver !== 'undefined'
1723

1824
export function useIntersection<T extends Element>({
25+
rootRef,
1926
rootMargin,
2027
disabled,
2128
}: UseIntersection): [(element: T | null) => void, boolean] {
2229
const isDisabled: boolean = disabled || !hasIntersectionObserver
2330

2431
const unobserve = useRef<Function>()
2532
const [visible, setVisible] = useState(false)
26-
33+
const [root, setRoot] = useState(rootRef ? rootRef.current : null)
2734
const setRef = useCallback(
2835
(el: T | null) => {
2936
if (unobserve.current) {
@@ -37,11 +44,11 @@ export function useIntersection<T extends Element>({
3744
unobserve.current = observe(
3845
el,
3946
(isVisible) => isVisible && setVisible(isVisible),
40-
{ rootMargin }
47+
{ root, rootMargin }
4148
)
4249
}
4350
},
44-
[isDisabled, rootMargin, visible]
51+
[isDisabled, root, rootMargin, visible]
4552
)
4653

4754
useEffect(() => {
@@ -53,6 +60,9 @@ export function useIntersection<T extends Element>({
5360
}
5461
}, [visible])
5562

63+
useEffect(() => {
64+
if (rootRef) setRoot(rootRef.current)
65+
}, [rootRef])
5666
return [setRef, visible]
5767
}
5868

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React, { useRef } from 'react'
2+
import Image from 'next/image'
3+
4+
const Page = () => {
5+
const myRef = useRef(null)
6+
7+
return (
8+
<>
9+
<div
10+
ref={myRef}
11+
style={{
12+
width: '100%',
13+
height: '400px',
14+
position: 'relative',
15+
overflowY: 'scroll',
16+
}}
17+
>
18+
<div style={{ width: '400px', height: '600px' }}>hello</div>
19+
<div style={{ width: '400px', position: 'relative', height: '600px' }}>
20+
<Image
21+
id="myImage"
22+
src="/test.jpg"
23+
width="400"
24+
height="400"
25+
lazyBoundary="1800px"
26+
/>
27+
</div>
28+
</div>
29+
</>
30+
)
31+
}
32+
export default Page
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React, { useRef } from 'react'
2+
import Image from 'next/image'
3+
4+
const Page = () => {
5+
const myRef = useRef(null)
6+
7+
return (
8+
<>
9+
<div
10+
ref={myRef}
11+
style={{
12+
width: '100%',
13+
height: '400px',
14+
position: 'relative',
15+
overflowY: 'scroll',
16+
}}
17+
>
18+
<div style={{ width: '400px', height: '600px' }}>hello</div>
19+
<div style={{ width: '400px', position: 'relative', height: '600px' }}>
20+
<Image
21+
lazyRoot={myRef}
22+
id="myImage"
23+
src="/test.jpg"
24+
width="400"
25+
height="400"
26+
lazyBoundary="1800px"
27+
/>
28+
</div>
29+
</div>
30+
</>
31+
)
32+
}
33+
export default Page

‎test/integration/image-component/default/test/index.test.js

+31
Original file line numberDiff line numberDiff line change
@@ -1008,6 +1008,37 @@ function runTests(mode) {
10081008
}
10091009
}
10101010
})
1011+
1012+
it('should load the image when the lazyRoot prop is used', async () => {
1013+
let browser
1014+
try {
1015+
//trying on '/lazy-noref' it fails
1016+
browser = await webdriver(appPort, '/lazy-withref')
1017+
1018+
await check(async () => {
1019+
const result = await browser.eval(
1020+
`document.getElementById('myImage').naturalWidth`
1021+
)
1022+
1023+
if (result < 400) {
1024+
throw new Error('Incorrectly loaded image')
1025+
}
1026+
1027+
return 'result-correct'
1028+
}, /result-correct/)
1029+
1030+
expect(
1031+
await hasImageMatchingUrl(
1032+
browser,
1033+
`http://localhost:${appPort}/_next/image?url=%2Ftest.jpg&w=828&q=75`
1034+
)
1035+
).toBe(true)
1036+
} finally {
1037+
if (browser) {
1038+
await browser.close()
1039+
}
1040+
}
1041+
})
10111042
}
10121043

10131044
describe('Image Component Tests', () => {

0 commit comments

Comments
 (0)
Please sign in to comment.