Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Image {...firstStubs} source={'https://test.com/img.jpg'} />
);
act(() => {
rerender(
<Image {...secondStubs} source={'https://test.com/img.jpg'} />
);
});
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(
<Image {...firstStubs} source={'https://test.com/img.jpg'} />
);
act(() => {
rerender(
<Image {...secondStubs} source={'https://test.com/img.jpg'} />
);
});
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();
});
});
});
});
20 changes: 19 additions & 1 deletion packages/react-native-web/src/exports/Image/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -288,6 +304,7 @@ const Image: React.AbstractComponent<
uri,
function load(e) {
updateState(LOADED);
const { onLoad, onLoadEnd } = loadEventHandlersRef.current;
if (onLoad) {
onLoad(e);
}
Expand All @@ -297,6 +314,7 @@ const Image: React.AbstractComponent<
},
function error() {
updateState(ERRORED);
const { onError, onLoadEnd } = loadEventHandlersRef.current;
if (onError) {
onError({
nativeEvent: {
Expand All @@ -319,7 +337,7 @@ const Image: React.AbstractComponent<
}

return abortPendingRequest;
}, [uri, requestRef, updateState, onError, onLoad, onLoadEnd, onLoadStart]);
}, [uri, requestRef, updateState]);

return (
<View
Expand Down
Loading