diff --git a/packages/react-native-web/src/exports/Image/__tests__/index-test.js b/packages/react-native-web/src/exports/Image/__tests__/index-test.js index 6bdd81337b..fe7779c3a5 100644 --- a/packages/react-native-web/src/exports/Image/__tests__/index-test.js +++ b/packages/react-native-web/src/exports/Image/__tests__/index-test.js @@ -401,4 +401,72 @@ describe('components/Image', () => { }); }); }); + + describe('load event handler identity', () => { + test('load is not restarted on update if only the handler identities change', () => { + const originalAbort = ImageLoader.abort; + ImageLoader.load = jest.fn(); + ImageLoader.abort = jest.fn(); + const firstStubs = { + onLoad: jest.fn(), + onLoadEnd: jest.fn(), + onLoadStart: jest.fn() + }; + const secondStubs = { + onLoad: jest.fn(), + onLoadEnd: jest.fn(), + onLoadStart: jest.fn() + }; + const { rerender } = render( + + ); + act(() => { + rerender( + + ); + }); + expect(ImageLoader.load.mock.calls.length).toBe(1); + expect(ImageLoader.abort).not.toHaveBeenCalled(); + expect(firstStubs.onLoadStart.mock.calls.length).toBe(1); + expect(secondStubs.onLoadStart).not.toHaveBeenCalled(); + ImageLoader.abort = originalAbort; + ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => { + onLoad(); + }); + }); + + test('most recent handlers are called when a load settles after an update', () => { + let resolveLoad; + ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => { + resolveLoad = onLoad; + }); + const firstStubs = { + onLoad: jest.fn(), + onLoadEnd: jest.fn(), + onLoadStart: jest.fn() + }; + const secondStubs = { + onLoad: jest.fn(), + onLoadEnd: jest.fn(), + onLoadStart: jest.fn() + }; + const { rerender } = render( + + ); + act(() => { + rerender( + + ); + }); + act(() => { + resolveLoad(); + }); + expect(firstStubs.onLoad).not.toHaveBeenCalled(); + expect(secondStubs.onLoad.mock.calls.length).toBe(1); + expect(secondStubs.onLoadEnd.mock.calls.length).toBe(1); + ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => { + onLoad(); + }); + }); + }); }); diff --git a/packages/react-native-web/src/exports/Image/index.js b/packages/react-native-web/src/exports/Image/index.js index f99faa7ec3..88f429e385 100644 --- a/packages/react-native-web/src/exports/Image/index.js +++ b/packages/react-native-web/src/exports/Image/index.js @@ -274,12 +274,28 @@ const Image: React.AbstractComponent< } // Image loading + // The load event handlers are read through a ref so that changes to their + // identity (e.g., inline functions re-created by a parent render) do not + // re-run the effect, which would abort the in-flight request, reset the + // state to LOADING, and call 'onLoadStart' again for the same uri. The + // handlers invoked are always the ones from the most recent render. + const loadEventHandlersRef = React.useRef({ + onError, + onLoad, + onLoadEnd, + onLoadStart + }); + React.useEffect(() => { + loadEventHandlersRef.current = { onError, onLoad, onLoadEnd, onLoadStart }; + }); + const uri = resolveAssetUri(source); React.useEffect(() => { abortPendingRequest(); if (uri != null) { updateState(LOADING); + const { onLoadStart } = loadEventHandlersRef.current; if (onLoadStart) { onLoadStart(); } @@ -288,6 +304,7 @@ const Image: React.AbstractComponent< uri, function load(e) { updateState(LOADED); + const { onLoad, onLoadEnd } = loadEventHandlersRef.current; if (onLoad) { onLoad(e); } @@ -297,6 +314,7 @@ const Image: React.AbstractComponent< }, function error() { updateState(ERRORED); + const { onError, onLoadEnd } = loadEventHandlersRef.current; if (onError) { onError({ nativeEvent: { @@ -319,7 +337,7 @@ const Image: React.AbstractComponent< } return abortPendingRequest; - }, [uri, requestRef, updateState, onError, onLoad, onLoadEnd, onLoadStart]); + }, [uri, requestRef, updateState]); return (