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 (