Skip to content

Commit 6900629

Browse files
refactor: image loading handlers (#6581)
* refactor: image loading handlers Some changes to clean up image loading code, and speed up subject image loading. - lib-classifier: remove `useEffect` from `useSubjectImage`, so that image loading code runs on image load, rather than waiting for the next React render cycle. - lib-react-components: when there's no delay for `useProgressiveImage`, run the `onLoad` handler immediately, rather than waiting for the next event cycle. - refactor `onLoad` event handlers to use `load` events, rather than `Image` objects. This brings the event handlers in line with the DOM standard. * add mock load events to classifier tests * fix: add frameIndex to onSubjectReady callback * add frame index to ImageAndText viewer
1 parent f61207d commit 6900629

File tree

18 files changed

+108
-46
lines changed

18 files changed

+108
-46
lines changed

packages/lib-classifier/src/components/Classifier/Classifier.spec.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,23 @@ describe('Components > Classifier', function () {
3838
constructor () {
3939
this.naturalHeight = 1000
4040
this.naturalWidth = 500
41-
setTimeout(() => this.onload(), 500)
41+
const fakeLoadEvent = {
42+
...new Event('load'),
43+
target: this
44+
}
45+
setTimeout(() => this.onload(fakeLoadEvent), 500)
4246
}
4347
}
4448

4549
class MockSlowImage {
4650
constructor () {
4751
this.naturalHeight = 1000
4852
this.naturalWidth = 500
49-
setTimeout(() => this.onload(), 5000)
53+
const fakeLoadEvent = {
54+
...new Event('load'),
55+
target: this
56+
}
57+
setTimeout(() => this.onload(fakeLoadEvent), 5000)
5058
}
5159
}
5260

packages/lib-classifier/src/components/Classifier/Classifier.workflows.spec.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ function testWorkflow(workflowSnapshot, workflowStrings) {
7676
constructor () {
7777
this.naturalHeight = 1000
7878
this.naturalWidth = 500
79-
setTimeout(() => this.onload(), 500)
79+
const fakeLoadEvent = {
80+
...new Event('load'),
81+
target: this
82+
}
83+
setTimeout(() => this.onload(fakeLoadEvent), 500)
8084
}
8185
})
8286

packages/lib-classifier/src/components/Classifier/ClassifierContainer.spec.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,12 @@ describe('components > ClassifierContainer', function () {
3333
constructor () {
3434
this.naturalHeight = 1000
3535
this.naturalWidth = 500
36+
const fakeLoadEvent = {
37+
...new Event('load'),
38+
target: this
39+
}
3640
setTimeout(() => {
37-
this.onload()
41+
this.onload(fakeLoadEvent)
3842
}, 500)
3943
}
4044
}

packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/FlipbookViewer/FlipbookViewer.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,13 @@ const FlipbookViewer = ({
3232
const [currentFrame, setCurrentFrame] = useState(defaultFrame)
3333
const [playing, setPlaying] = useState(false)
3434
const [dragMove, setDragMove] = useState()
35-
/** This initializes an image element from the subject's defaultFrame src url.
35+
/** This initializes an image element from the subject's currentFrame src url.
3636
* We do this so the SVGPanZoom has dimensions of the subject image.
37-
* We're assuming all frames in one subject have the same dimensions. */
38-
const defaultFrameLocation = subject ? subject.locations[defaultFrame] : null
37+
*/
38+
const currentFrameLocation = subject ? subject.locations[currentFrame] : null
3939
const { img, error, loading, subjectImage } = useSubjectImage({
40-
src: defaultFrameLocation.url,
40+
frame: currentFrame,
41+
src: currentFrameLocation.url,
4142
onReady,
4243
onError
4344
})

packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/ImageAndTextViewer/ImageAndTextViewerContainer.js

+2
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,15 @@ function ImageAndTextViewerContainer ({
4848
{(type === 'text')
4949
? (
5050
<SingleTextViewer
51+
frame={frame}
5152
height={dimensions[0]?.clientHeight ? `${dimensions[0]?.clientHeight}px` : ''}
5253
onError={onError}
5354
onReady={onReady}
5455
/>)
5556
: (
5657
<SingleImageViewer
5758
enableInteractionLayer={false}
59+
frame={frame}
5860
loadingState={loadingState}
5961
onError={onError}
6062
onReady={onReady}

packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/MultiFrameViewer/MultiFrameViewerContainer.js

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ function MultiFrameViewerContainer({
7777
// TODO: replace this with a better function to parse the image location from a subject.
7878
const imageLocation = subject ? subject.locations[frame] : null
7979
const { img, error, loading, subjectImage } = useSubjectImage({
80+
frame,
8081
src: imageLocation?.url,
8182
onReady,
8283
onError

packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/MultiFrameViewer/MultiFrameViewerContainer.spec.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ describe('Component > MultiFrameViewerContainer', function () {
2626
constructor () {
2727
this.naturalHeight = height
2828
this.naturalWidth = width
29-
setTimeout(() => this.onload(), DELAY)
29+
const fakeLoadEvent = {
30+
...new Event('load'),
31+
target: this
32+
}
33+
setTimeout(() => this.onload(fakeLoadEvent), DELAY)
3034
}
3135
}
3236

packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/SeparateFramesViewer/components/SeparateFrame/SeparateFrame.js

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const SeparateFrame = ({
3232
onReady = DEFAULT_HANDLER
3333
}) => {
3434
const { img, error, loading, subjectImage } = useSubjectImage({
35+
frame,
3536
src: frameUrl,
3637
onReady,
3738
onError

packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/SingleImageViewer/SingleImageViewerContainer.spec.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ describe('Component > SingleImageViewerContainer', function () {
2424
constructor () {
2525
this.naturalHeight = height
2626
this.naturalWidth = width
27-
setTimeout(() => this.onload(), DELAY)
27+
const fakeLoadEvent = {
28+
...new Event('load'),
29+
target: this
30+
}
31+
setTimeout(() => this.onload(fakeLoadEvent), DELAY)
2832
}
2933
}
3034

packages/lib-classifier/src/components/Classifier/components/SubjectViewer/components/SingleTextViewer/SingleTextViewerContainer.js

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ function DEFAULT_HANDLER() {
4949
}
5050

5151
export function SingleTextViewerContainer ({
52+
frame = 0,
5253
height = '',
5354
latest,
5455
loadingState = asyncStates.initialized,
@@ -57,6 +58,7 @@ export function SingleTextViewerContainer ({
5758
subject = defaultSubject
5859
}) {
5960
const { data, error, loading } = useSubjectText({
61+
frame,
6062
subject,
6163
onReady,
6264
onError

packages/lib-classifier/src/hooks/Readme.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ A custom hook that fetches an image from a URL, with a ref to the image's DOM no
104104
Usage:
105105
```jsx
106106
// img is a DOM img. subjectImage is a React ref to the element that displays the image.
107-
const { img, error, loading, subjectImage } = useSubjectImage({ src, onReady, onError })
107+
const { img, error, loading, subjectImage } = useSubjectImage({ frame, src, onReady, onError })
108108

109109
if (loading) {
110110
return <p>The image is still loading.</p>
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,28 @@
11
import { useEffect, useRef } from 'react'
22
import { useProgressiveImage } from '@zooniverse/react-components/hooks'
33

4-
export default function useSubjectImage({ src, onReady, onError }) {
4+
export default function useSubjectImage({ frame = 0, src, onReady, onError }) {
55
const subjectImage = useRef()
66

77
const { img, error, loading } = useProgressiveImage({
88
placeholderSrc: '',
9-
src
9+
src,
10+
onLoad,
11+
onError
1012
})
1113

1214

13-
useEffect(function onImageLoad() {
14-
const { naturalHeight, naturalWidth, src } = img
15+
function onLoad(event) {
16+
const { naturalHeight, naturalWidth, src } = event.target
1517
if (src !== '' ) {
1618
const svgImage = subjectImage.current
1719
const { width: clientWidth, height: clientHeight } = svgImage
1820
? svgImage.getBoundingClientRect()
1921
: {}
2022
const target = { clientHeight, clientWidth, naturalHeight, naturalWidth }
21-
onReady({ target })
23+
onReady({ target }, frame)
2224
}
23-
}, [img, onReady, subjectImage])
24-
25-
useEffect(function logError() {
26-
if (!loading && error) {
27-
console.error(error)
28-
onError(error)
29-
}
30-
}, [error, loading, onError])
25+
}
3126

3227
return { img, error, loading, subjectImage }
3328
}

packages/lib-classifier/src/hooks/useSubjectText.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const DEFAULT_HANDLER = function () {
3131
A custom hook to load text data subjects from Panoptes
3232
*/
3333
export default function useSubjectText({
34+
/** location frame index */
35+
frame = 0,
3436
/** Panoptes subject */
3537
subject,
3638
/** on data ready callback */
@@ -44,7 +46,7 @@ export default function useSubjectText({
4446
useEffect(function onSubjectChange() {
4547
function onLoad(rawData) {
4648
setData(rawData)
47-
onReady()
49+
onReady(null, frame)
4850
}
4951

5052
async function handleSubject() {

packages/lib-classifier/src/store/SubjectViewerStore/SubjectViewerStore.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -115,15 +115,15 @@ const SubjectViewer = types
115115
self.loadingState = asyncStates.error
116116
},
117117

118-
onSubjectReady (event) {
118+
onSubjectReady (event, frameIndex = 0) {
119119
const { target = {} } = event || {}
120120
const {
121121
clientHeight = 0,
122122
clientWidth = 0,
123123
naturalHeight = 0,
124124
naturalWidth = 0
125125
} = target || {}
126-
self.dimensions.push({ clientHeight, clientWidth, naturalHeight, naturalWidth })
126+
self.dimensions[frameIndex] = { clientHeight, clientWidth, naturalHeight, naturalWidth }
127127
self.rotation = 0
128128
self.loadingState = asyncStates.success
129129
},

packages/lib-react-components/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1313
- PlainButton has text-decoration underline on hover even for links.
1414
- ZooFooter links and labels updated to reflect newly launched FEM pages.
1515
- Updated styling in ProjectCard's badge component.
16+
- `useProgressiveImage`: run `onLoad` callbacks immediately when `delay` is 0. Add optional `onLoad` and `onError` event handlers for image `load` events.
1617

1718
### Fixed
1819

packages/lib-react-components/src/Media/components/ThumbnailImage/ThumbnailImage.spec.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ describe('ThumbnailImage', function () {
1212
constructor () {
1313
this.naturalHeight = 200
1414
this.naturalWidth = 400
15-
setTimeout(() => this.onload(), 0)
15+
const fakeLoadEvent = {
16+
...new Event('load'),
17+
target: this
18+
}
19+
setTimeout(() => this.onload(fakeLoadEvent), 0)
1620
}
1721
}
1822

packages/lib-react-components/src/hooks/Readme.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,17 @@ const { data: user, error, isLoading } = usePanoptesUser()
4747
Use a placeholder while an image downloads.
4848

4949
```jsx
50+
function onLoad(event) {
51+
console.log('image loaded: ', event.target)
52+
}
53+
54+
function onError(error) {
55+
console.warn('loading failed')
56+
console.error(error)
57+
}
58+
5059
const src = 'https://panoptes-uploads.zooniverse.org/production/subject_location/66094a64-8823-4314-8ef4-1ee228e49470.jpeg'
51-
const { img, error, loading } = useProgressiveImage({ delay: 0, src })
60+
const { img, error, loading } = useProgressiveImage({ delay: 0, src, onLoad, onError })
5261

5362
return <img src={img.src} alt='This is an example of an image with a placeholder.'/>
5463
```
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,60 @@
1-
import { useEffect, useState } from 'react'
1+
import { useEffect, useRef, useState } from 'react'
22

33
const defaultPlaceholder = {
44
naturalHeight: 600,
55
naturalWidth: 800,
66
src: ''
77
}
88

9+
const DEFAULT_HANDLER = () => false
10+
11+
function preloadImage({
12+
src,
13+
onLoad = DEFAULT_HANDLER,
14+
onError = DEFAULT_HANDLER
15+
}) {
16+
const { Image } = window
17+
const img = new Image()
18+
img.onload = onLoad
19+
img.onerror = onError
20+
img.src = src
21+
}
22+
923
export default function useProgressiveImage({
1024
delay = 0,
1125
placeholderSrc = '',
12-
src
26+
src,
27+
onLoad = DEFAULT_HANDLER,
28+
onError = DEFAULT_HANDLER
1329
}) {
1430
const placeholder = {
1531
...defaultPlaceholder,
1632
src: placeholderSrc
1733
}
1834
const [loading, setLoading] = useState(true)
19-
const [img, setImg] = useState(placeholder)
35+
const imgRef = useRef(placeholder)
2036
const [error, setError] = useState(null)
2137

38+
function onImageLoad(event) {
39+
imgRef.current = event.target
40+
onLoad(event)
41+
setLoading(false)
42+
}
43+
44+
function onImageError(error) {
45+
onError(error)
46+
setError(error)
47+
setLoading(false)
48+
}
49+
2250
function fetchImage() {
23-
const { Image } = window
24-
const img = new Image()
25-
img.onload = () => {
26-
setTimeout(() => {
27-
setImg(img)
28-
setLoading(false)
29-
}, delay)
30-
}
31-
img.onerror = (error) => {
32-
setError(error)
33-
setLoading(false)
34-
}
3551
setLoading(true)
3652
setError(null)
37-
img.src = src
53+
preloadImage({
54+
src,
55+
onLoad: delay ? event => setTimeout(onImageLoad, delay, event) : onImageLoad,
56+
onError: onImageError
57+
})
3858
}
3959

4060
useEffect(function onNewImage() {
@@ -43,5 +63,5 @@ export default function useProgressiveImage({
4363
}
4464
}, [src])
4565

46-
return { img, error, loading }
66+
return { img: imgRef.current, error, loading }
4767
}

0 commit comments

Comments
 (0)