Skip to content

Commit d243ba1

Browse files
Merge branch 'canary' into shu/d205
2 parents 95c7525 + c879fce commit d243ba1

File tree

3 files changed

+188
-85
lines changed

3 files changed

+188
-85
lines changed

packages/next/client/image.tsx

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react'
1+
import React, { useRef, useEffect } from 'react'
22
import Head from '../shared/lib/head'
33
import {
44
ImageConfigComplete,
@@ -8,7 +8,6 @@ import {
88
} from '../server/image-config'
99
import { useIntersection } from './use-intersection'
1010

11-
const loadedImageURLs = new Set<string>()
1211
const allImgs = new Map<
1312
string,
1413
{ src: string; priority: boolean; placeholder: string }
@@ -250,31 +249,34 @@ function defaultImageLoader(loaderProps: ImageLoaderProps) {
250249
// See https://stackoverflow.com/q/39777833/266535 for why we use this ref
251250
// handler instead of the img's onLoad attribute.
252251
function handleLoading(
253-
img: HTMLImageElement | null,
252+
imgRef: React.RefObject<HTMLImageElement>,
254253
src: string,
255254
layout: LayoutValue,
256255
placeholder: PlaceholderValue,
257-
onLoadingComplete?: OnLoadingComplete
256+
onLoadingCompleteRef: React.MutableRefObject<OnLoadingComplete | undefined>
258257
) {
259-
if (!img) {
260-
return
261-
}
262258
const handleLoad = () => {
259+
const img = imgRef.current
260+
if (!img) {
261+
return
262+
}
263263
if (img.src !== emptyDataURL) {
264264
const p = 'decode' in img ? img.decode() : Promise.resolve()
265265
p.catch(() => {}).then(() => {
266+
if (!imgRef.current) {
267+
return
268+
}
266269
if (placeholder === 'blur') {
267270
img.style.filter = ''
268271
img.style.backgroundSize = ''
269272
img.style.backgroundImage = ''
270273
img.style.backgroundPosition = ''
271274
}
272-
loadedImageURLs.add(src)
273-
if (onLoadingComplete) {
275+
if (onLoadingCompleteRef.current) {
274276
const { naturalWidth, naturalHeight } = img
275277
// Pass back read-only primitive values but not the
276278
// underlying DOM element because it could be misused.
277-
onLoadingComplete({ naturalWidth, naturalHeight })
279+
onLoadingCompleteRef.current({ naturalWidth, naturalHeight })
278280
}
279281
if (process.env.NODE_ENV !== 'production') {
280282
if (img.parentElement?.parentElement) {
@@ -299,13 +301,15 @@ function handleLoading(
299301
})
300302
}
301303
}
302-
if (img.complete) {
303-
// If the real image fails to load, this will still remove the placeholder.
304-
// This is the desired behavior for now, and will be revisited when error
305-
// handling is worked on for the image component itself.
306-
handleLoad()
307-
} else {
308-
img.onload = handleLoad
304+
if (imgRef.current) {
305+
if (imgRef.current.complete) {
306+
// If the real image fails to load, this will still remove the placeholder.
307+
// This is the desired behavior for now, and will be revisited when error
308+
// handling is worked on for the image component itself.
309+
handleLoad()
310+
} else {
311+
imgRef.current.onload = handleLoad
312+
}
309313
}
310314
}
311315

@@ -328,6 +332,7 @@ export default function Image({
328332
blurDataURL,
329333
...all
330334
}: ImageProps) {
335+
const imgRef = useRef<HTMLImageElement>(null)
331336
let rest: Partial<ImageProps> = all
332337
let layout: NonNullable<LayoutValue> = sizes ? 'responsive' : 'intrinsic'
333338
if ('layout' in rest) {
@@ -376,7 +381,7 @@ export default function Image({
376381
unoptimized = true
377382
isLazy = false
378383
}
379-
if (typeof window !== 'undefined' && loadedImageURLs.has(src)) {
384+
if (typeof window !== 'undefined' && imgRef.current?.complete) {
380385
isLazy = false
381386
}
382387

@@ -504,7 +509,7 @@ export default function Image({
504509
}
505510
}
506511

507-
const [setRef, isIntersected] = useIntersection<HTMLImageElement>({
512+
const [setIntersection, isIntersected] = useIntersection<HTMLImageElement>({
508513
rootMargin: lazyBoundary,
509514
disabled: !isLazy,
510515
})
@@ -657,6 +662,22 @@ export default function Image({
657662
[imageSizesPropName]: imgAttributes.sizes,
658663
}
659664

665+
const useLayoutEffect =
666+
typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect
667+
const onLoadingCompleteRef = useRef(onLoadingComplete)
668+
669+
useEffect(() => {
670+
onLoadingCompleteRef.current = onLoadingComplete
671+
}, [onLoadingComplete])
672+
673+
useLayoutEffect(() => {
674+
setIntersection(imgRef.current)
675+
}, [setIntersection])
676+
677+
useEffect(() => {
678+
handleLoading(imgRef, srcString, layout, placeholder, onLoadingCompleteRef)
679+
}, [srcString, layout, placeholder, isVisible])
680+
660681
return (
661682
<span style={wrapperStyle}>
662683
{hasSizer ? (
@@ -687,10 +708,7 @@ export default function Image({
687708
decoding="async"
688709
data-nimg={layout}
689710
className={className}
690-
ref={(img) => {
691-
setRef(img)
692-
handleLoading(img, srcString, layout, placeholder, onLoadingComplete)
693-
}}
711+
ref={imgRef}
694712
style={{ ...imgStyle, ...blurStyle }}
695713
/>
696714
{isLazy && (

test/integration/image-component/default/pages/on-loading-complete.js

Lines changed: 99 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,122 @@
11
import { useState } from 'react'
22
import Image from 'next/image'
33

4-
const Page = () => (
5-
<div>
6-
<h1>On Loading Complete Test</h1>
7-
<ImageWithMessage
8-
id="1"
9-
src="/test.jpg"
10-
layout="intrinsic"
11-
width="128"
12-
height="128"
13-
/>
14-
<hr />
15-
<ImageWithMessage
16-
id="2"
17-
src={require('../public/test.png')}
18-
placeholder="blur"
19-
layout="fixed"
20-
/>
21-
<hr />
22-
<ImageWithMessage
23-
id="3"
24-
src="/test.svg"
25-
layout="responsive"
26-
width="1200"
27-
height="1200"
28-
/>
29-
<hr />
30-
<div style={{ position: 'relative', width: '64px', height: '64px' }}>
4+
const Page = () => {
5+
// Hoisted state to count each image load callback
6+
const [idToCount, setIdToCount] = useState({})
7+
const [clicked, setClicked] = useState(false)
8+
9+
return (
10+
<div>
11+
<h1>On Loading Complete Test</h1>
12+
<ImageWithMessage
13+
id="1"
14+
src="/test.jpg"
15+
layout="intrinsic"
16+
width="128"
17+
height="128"
18+
idToCount={idToCount}
19+
setIdToCount={setIdToCount}
20+
/>
21+
22+
<ImageWithMessage
23+
id="2"
24+
src={require('../public/test.png')}
25+
placeholder="blur"
26+
layout="fixed"
27+
idToCount={idToCount}
28+
setIdToCount={setIdToCount}
29+
/>
30+
31+
<ImageWithMessage
32+
id="3"
33+
src="/test.svg"
34+
layout="responsive"
35+
width="1200"
36+
height="1200"
37+
idToCount={idToCount}
38+
setIdToCount={setIdToCount}
39+
/>
40+
3141
<ImageWithMessage
3242
id="4"
3343
src="/test.ico"
3444
layout="fill"
3545
objectFit="contain"
46+
idToCount={idToCount}
47+
setIdToCount={setIdToCount}
3648
/>
37-
</div>
38-
<hr />
39-
<ImageWithMessage
40-
id="5"
41-
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAE0lEQVR42mNk+P+/ngEKGMngAADDdwx3Uz/3AAAAAABJRU5ErkJggg=="
42-
layout="fixed"
43-
width={64}
44-
height={64}
45-
/>
46-
<hr />
47-
<div style={{ position: 'relative', width: '64px', height: '64px' }}>
49+
50+
<ImageWithMessage
51+
id="5"
52+
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAE0lEQVR42mNk+P+/ngEKGMngAADDdwx3Uz/3AAAAAABJRU5ErkJggg=="
53+
layout="fixed"
54+
width={64}
55+
height={64}
56+
idToCount={idToCount}
57+
setIdToCount={setIdToCount}
58+
/>
59+
4860
<ImageWithMessage
4961
id="6"
5062
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAE0lEQVR42mNk+P+/ngEKGMngAADDdwx3Uz/3AAAAAABJRU5ErkJggg=="
5163
layout="fill"
64+
idToCount={idToCount}
65+
setIdToCount={setIdToCount}
66+
/>
67+
68+
<ImageWithMessage
69+
id="7"
70+
src="/test.bmp"
71+
loading="eager"
72+
width="400"
73+
height="400"
74+
idToCount={idToCount}
75+
setIdToCount={setIdToCount}
5276
/>
77+
78+
<ImageWithMessage
79+
id="8"
80+
src={clicked ? '/foo/test-rect.jpg' : '/wide.png'}
81+
layout={clicked ? 'fixed' : 'intrinsic'}
82+
width="500"
83+
height="500"
84+
idToCount={idToCount}
85+
setIdToCount={setIdToCount}
86+
/>
87+
<button id="toggle" onClick={() => setClicked(!clicked)}>
88+
Toggle
89+
</button>
90+
<div id="footer" />
5391
</div>
54-
<div id="footer" />
55-
</div>
56-
)
92+
)
93+
}
5794

58-
function ImageWithMessage({ id, ...props }) {
95+
function ImageWithMessage({ id, idToCount, setIdToCount, ...props }) {
5996
const [msg, setMsg] = useState('[LOADING]')
97+
const style =
98+
props.layout === 'fill'
99+
? { position: 'relative', width: '64px', height: '64px' }
100+
: {}
60101
return (
61102
<>
62-
<Image
63-
id={`img${id}`}
64-
onLoadingComplete={({ naturalWidth, naturalHeight }) =>
65-
setMsg(
66-
`loaded img${id} with dimensions ${naturalWidth}x${naturalHeight}`
67-
)
68-
}
69-
{...props}
70-
/>
103+
<div className="wrap" style={style}>
104+
<Image
105+
id={`img${id}`}
106+
onLoadingComplete={({ naturalWidth, naturalHeight }) => {
107+
let count = idToCount[id] || 0
108+
count++
109+
idToCount[id] = count
110+
setIdToCount(idToCount)
111+
const msg = `loaded ${count} img${id} with dimensions ${naturalWidth}x${naturalHeight}`
112+
setMsg(msg)
113+
console.log(msg)
114+
}}
115+
{...props}
116+
/>
117+
</div>
71118
<p id={`msg${id}`}>{msg}</p>
119+
<hr />
72120
</>
73121
)
74122
}

0 commit comments

Comments
 (0)