Skip to content

Commit 400fc11

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 f854992 commit 400fc11

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';
@@ -312,9 +312,10 @@ type UseSourceParams = {
312312
const useSource = (
313313
{ onLoad, onLoadStart, onLoadEnd, onError }: UseSourceParams,
314314
source: ?Source
315-
): { state: string, uri: string } => {
315+
): { state: string, loadedUri: string } => {
316+
const lastLoadedSource = React.useRef();
316317
const [loadedUri, setLoadedUri] = React.useState('');
317-
const [state, updateState] = React.useState(() => {
318+
const [state, setState] = React.useState(() => {
318319
const uri = resolveAssetUri(source);
319320
if (uri != null) {
320321
const isLoaded = ImageLoader.has(uri);
@@ -323,39 +324,50 @@ const useSource = (
323324
return IDLE;
324325
});
325326

326-
const loadInput = React.useRef(null);
327-
328-
React.useEffect(() => {
327+
// This object would only change when load related fields change
328+
// We try to maintain strict object reference to prevent the effect hook running due to object change
329+
const stableSource = React.useMemo(() => {
329330
const uri = resolveAssetUri(source);
330-
if (uri == null) return;
331+
if (uri == null) {
332+
lastLoadedSource.current = null;
333+
return null;
334+
}
331335

332336
let headers;
333337
if (source && typeof source.headers === 'object') {
334338
headers = ((source.headers: any): { [key: string]: string });
335339
}
336340

337341
const nextInput = { uri, headers };
338-
const currentInput = loadInput.current;
339-
if (JSON.stringify(nextInput) === JSON.stringify(currentInput)) return;
342+
if (
343+
JSON.stringify(nextInput) !== JSON.stringify(lastLoadedSource.current)
344+
) {
345+
lastLoadedSource.current = nextInput;
346+
}
347+
348+
return lastLoadedSource.current;
349+
}, [source]);
350+
351+
React.useEffect(() => {
352+
if (stableSource == null) return;
340353

341-
updateState(LOADING);
354+
setState(LOADING);
342355
if (onLoadStart) onLoadStart();
343356

344-
loadInput.current = nextInput;
345357
const requestId = ImageLoader.load(
346-
nextInput,
358+
stableSource,
347359
function load(result) {
348-
updateState(LOADED);
360+
setState(LOADED);
349361
setLoadedUri(result.uri);
350362
if (onLoad) onLoad(result);
351363
if (onLoadEnd) onLoadEnd();
352364
},
353365
function error() {
354-
updateState(ERRORED);
366+
setState(ERRORED);
355367
if (onError) {
356368
onError({
357369
nativeEvent: {
358-
error: `Failed to load resource ${uri} (404)`
370+
error: `Failed to load resource ${stableSource.uri} (404)`
359371
}
360372
});
361373
}
@@ -365,7 +377,7 @@ const useSource = (
365377

366378
const effectCleanup = () => ImageLoader.release(requestId);
367379
return effectCleanup;
368-
}, [updateState, onError, onLoad, onLoadEnd, onLoadStart, source]);
380+
}, [onError, onLoad, onLoadEnd, onLoadStart, stableSource]);
369381

370382
return {
371383
state,

0 commit comments

Comments
 (0)