Skip to content

Commit 78d9651

Browse files
committed
[change] Image - handle cases where the source object only changes by reference
When the source object changed by reference, but stays structurally the same we should do nothing and not trigger the loading effect again
1 parent ae00c59 commit 78d9651

File tree

2 files changed

+43
-16
lines changed

2 files changed

+43
-16
lines changed

packages/react-native-web/src/exports/Image/__tests__/index-test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,21 @@ describe('components/Image', () => {
432432
expect.any(Function)
433433
);
434434
});
435+
436+
// A common case is `source` declared as an inline object, which cause is to be a
437+
// new object (with the same content) each time parent component renders
438+
test('it still loads the image if source object is changed', () => {
439+
ImageLoader.load.mockImplementation(() => {});
440+
441+
const releaseSpy = jest.spyOn(ImageLoader, 'release');
442+
443+
const uri = 'https://google.com/favicon.ico';
444+
const { rerender } = render(<Image source={{ uri }} />);
445+
rerender(<Image source={{ uri }} />);
446+
447+
// when the underlying source didn't change we expect the initial request is not cancelled due to re-render
448+
expect(releaseSpy).not.toHaveBeenCalled();
449+
});
435450
});
436451

437452
describe('prop "style"', () => {

packages/react-native-web/src/exports/Image/index.js

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* @flow
99
*/
1010

11-
import type { ImageProps } from './types';
11+
import type { ImageProps, Source } from './types';
1212

1313
import * as React from 'react';
1414
import createElement from '../createElement';
@@ -317,9 +317,10 @@ type UseSourceParams = {
317317
const useSource = (
318318
{ onLoad, onLoadStart, onLoadEnd, onError }: UseSourceParams,
319319
source: ?Source
320-
): { state: string, uri: string } => {
320+
): { state: string, loadedUri: string } => {
321+
const lastLoadedSource = React.useRef();
321322
const [loadedUri, setLoadedUri] = React.useState('');
322-
const [state, updateState] = React.useState(() => {
323+
const [state, setState] = React.useState(() => {
323324
const uri = resolveAssetUri(source);
324325
if (uri != null) {
325326
const isLoaded = ImageLoader.has(uri);
@@ -328,39 +329,50 @@ const useSource = (
328329
return IDLE;
329330
});
330331

331-
const loadInput = React.useRef(null);
332-
333-
React.useEffect(() => {
332+
// This object would only change when load related fields change
333+
// We try to maintain strict object reference to prevent the effect hook running due to object change
334+
const stableSource = React.useMemo(() => {
334335
const uri = resolveAssetUri(source);
335-
if (uri == null) return;
336+
if (uri == null) {
337+
lastLoadedSource.current = null;
338+
return null;
339+
}
336340

337341
let headers;
338342
if (source && typeof source.headers === 'object') {
339343
headers = ((source.headers: any): { [key: string]: string });
340344
}
341345

342346
const nextInput = { uri, headers };
343-
const currentInput = loadInput.current;
344-
if (JSON.stringify(nextInput) === JSON.stringify(currentInput)) return;
347+
if (
348+
JSON.stringify(nextInput) !== JSON.stringify(lastLoadedSource.current)
349+
) {
350+
lastLoadedSource.current = nextInput;
351+
}
352+
353+
return lastLoadedSource.current;
354+
}, [source]);
355+
356+
React.useEffect(() => {
357+
if (stableSource == null) return;
345358

346-
updateState(LOADING);
359+
setState(LOADING);
347360
if (onLoadStart) onLoadStart();
348361

349-
loadInput.current = nextInput;
350362
const requestId = ImageLoader.load(
351-
nextInput,
363+
stableSource,
352364
function load(result) {
353-
updateState(LOADED);
365+
setState(LOADED);
354366
setLoadedUri(result.uri);
355367
if (onLoad) onLoad(result);
356368
if (onLoadEnd) onLoadEnd();
357369
},
358370
function error() {
359-
updateState(ERRORED);
371+
setState(ERRORED);
360372
if (onError) {
361373
onError({
362374
nativeEvent: {
363-
error: `Failed to load resource ${uri} (404)`
375+
error: `Failed to load resource ${stableSource.uri} (404)`
364376
}
365377
});
366378
}
@@ -370,7 +382,7 @@ const useSource = (
370382

371383
const effectCleanup = () => ImageLoader.release(requestId);
372384
return effectCleanup;
373-
}, [updateState, onError, onLoad, onLoadEnd, onLoadStart, source]);
385+
}, [onError, onLoad, onLoadEnd, onLoadStart, stableSource]);
374386

375387
return {
376388
state,

0 commit comments

Comments
 (0)