Skip to content

Commit 16984f8

Browse files
committed
[change] Image: revert changes and use one component
1 parent 65b45fd commit 16984f8

File tree

4 files changed

+49
-117
lines changed

4 files changed

+49
-117
lines changed

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

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ const originalImage = window.Image;
2020
describe('components/Image', () => {
2121
beforeEach(() => {
2222
ImageUriCache._entries = {};
23+
ImageLoader.load = jest.fn((_, onLoad, onError) => {
24+
onLoad();
25+
return { cancel: jest.fn() };
26+
});
2327
window.Image = jest.fn(() => ({}));
2428
});
2529

@@ -107,9 +111,6 @@ describe('components/Image', () => {
107111
describe('prop "onLoad"', () => {
108112
test('is called after image is loaded from network', () => {
109113
jest.useFakeTimers();
110-
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
111-
onLoad();
112-
});
113114
const onLoadStartStub = jest.fn();
114115
const onLoadStub = jest.fn();
115116
const onLoadEndStub = jest.fn();
@@ -127,9 +128,6 @@ describe('components/Image', () => {
127128

128129
test('is called after image is loaded from cache', () => {
129130
jest.useFakeTimers();
130-
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
131-
onLoad();
132-
});
133131
const onLoadStartStub = jest.fn();
134132
const onLoadStub = jest.fn();
135133
const onLoadEndStub = jest.fn();
@@ -261,11 +259,6 @@ describe('components/Image', () => {
261259

262260
test('is set immediately if the image was preloaded', () => {
263261
const uri = 'https://yahoo.com/favicon.ico';
264-
ImageLoader.load = jest
265-
.fn()
266-
.mockImplementationOnce((_, onLoad, onError) => {
267-
onLoad();
268-
});
269262
return Image.prefetch(uri).then(() => {
270263
const source = { uri };
271264
const { container } = render(<Image source={source} />, {
@@ -313,6 +306,7 @@ describe('components/Image', () => {
313306
.fn()
314307
.mockImplementationOnce((_, onLoad, onError) => {
315308
loadCallback = onLoad;
309+
return { cancel: jest.fn() };
316310
});
317311
const { container } = render(
318312
<Image defaultSource={{ uri: defaultUri }} source={{ uri }} />

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

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

11-
import type { ImageProps, SourceObject } from './types';
11+
import type { LoadRequest } from '../../modules/ImageLoader';
12+
import type { ImageProps } from './types';
1213

1314
import * as React from 'react';
1415
import createElement from '../createElement';
@@ -146,7 +147,7 @@ function resolveAssetUri(source): ?string {
146147
return uri;
147148
}
148149

149-
function hasSourceDiff(a: SourceObject, b: SourceObject) {
150+
function hasSourceDiff(a, b) {
150151
return (
151152
a.uri !== b.uri || JSON.stringify(a.headers) !== JSON.stringify(b.headers)
152153
);
@@ -164,12 +165,10 @@ interface ImageStatics {
164165
) => Promise<{| [uri: string]: 'disk/memory' |}>;
165166
}
166167

167-
type ImageComponent = React.AbstractComponent<
168+
const Image: React.AbstractComponent<
168169
ImageProps,
169170
React.ElementRef<typeof View>
170-
>;
171-
172-
const BaseImage: ImageComponent = React.forwardRef((props, ref) => {
171+
> = React.forwardRef((props, ref) => {
173172
const {
174173
accessibilityLabel,
175174
blurRadius,
@@ -194,6 +193,7 @@ const BaseImage: ImageComponent = React.forwardRef((props, ref) => {
194193
}
195194
}
196195

196+
const lastLoadedSource = React.useRef({});
197197
const [state, updateState] = React.useState(() => {
198198
const uri = resolveAssetUri(source);
199199
if (uri != null) {
@@ -209,7 +209,10 @@ const BaseImage: ImageComponent = React.forwardRef((props, ref) => {
209209
const hasTextAncestor = React.useContext(TextAncestorContext);
210210
const hiddenImageRef = React.useRef(null);
211211
const filterRef = React.useRef(_filterId++);
212-
const requestRef = React.useRef(null);
212+
const requestRef = React.useRef<LoadRequest>({
213+
cancel: () => {},
214+
requestId: -1
215+
});
213216
const shouldDisplaySource =
214217
state === LOADED || (state === LOADING && defaultSource == null);
215218
const [flatStyle, _resizeMode, filter, tintColor] = getFlatStyle(
@@ -266,16 +269,27 @@ const BaseImage: ImageComponent = React.forwardRef((props, ref) => {
266269
// Image loading
267270
const uri = resolveAssetUri(source);
268271
React.useEffect(() => {
269-
abortPendingRequest();
270-
271272
if (uri != null) {
273+
const nextSource = {
274+
// $FlowFixMe
275+
headers: source.headers,
276+
uri
277+
};
278+
if (!hasSourceDiff(nextSource, lastLoadedSource.current)) return;
279+
280+
requestRef.current.cancel();
272281
updateState(LOADING);
273282
if (onLoadStart) {
274283
onLoadStart();
275284
}
276285

277-
requestRef.current = ImageLoader.load(
278-
uri,
286+
const makeRequest = nextSource.headers
287+
? ImageLoader.loadWithHeaders
288+
: ImageLoader.load;
289+
290+
requestRef.current = makeRequest(
291+
// $FlowFixMe
292+
nextSource,
279293
function load(e) {
280294
updateState(LOADED);
281295
if (onLoad) {
@@ -300,17 +314,11 @@ const BaseImage: ImageComponent = React.forwardRef((props, ref) => {
300314
}
301315
);
302316
}
303-
304-
function abortPendingRequest() {
305-
if (requestRef.current != null) {
306-
ImageLoader.abort(requestRef.current);
307-
requestRef.current = null;
308-
}
309-
}
310-
311-
return abortPendingRequest;
312317
}, [uri, requestRef, updateState, onError, onLoad, onLoadEnd, onLoadStart]);
313318

319+
// Run the cleanup function on unmount
320+
React.useEffect(() => requestRef.current.cancel, []);
321+
314322
return (
315323
<View
316324
{...rest}
@@ -340,94 +348,24 @@ const BaseImage: ImageComponent = React.forwardRef((props, ref) => {
340348
);
341349
});
342350

343-
/**
344-
* This component handles specifically loading an image source with header
345-
*/
346-
const ImageWithHeaders: ImageComponent = React.forwardRef((props, ref) => {
347-
// $FlowIgnore
348-
const nextSource: SourceObject = props.source;
349-
const prevSource = React.useRef<SourceObject>({});
350-
const cleanup = React.useRef<Function>(() => {});
351-
const [blobUri, setBlobUri] = React.useState('');
352-
353-
const { onError, onLoadStart } = props;
354-
355-
React.useEffect(() => {
356-
if (!hasSourceDiff(nextSource, prevSource.current)) return;
357-
358-
// When source changes we want to clean up any old/running requests
359-
cleanup.current();
360-
361-
prevSource.current = nextSource;
362-
363-
let uri;
364-
const abortCtrl = new AbortController();
365-
const request = new Request(nextSource.uri, {
366-
headers: nextSource.headers,
367-
signal: abortCtrl.signal
368-
});
369-
request.headers.append('accept', 'image/*');
370-
371-
if (onLoadStart) onLoadStart();
372-
373-
fetch(request)
374-
.then((response) => response.blob())
375-
.then((blob) => {
376-
uri = URL.createObjectURL(blob);
377-
setBlobUri(uri);
378-
})
379-
.catch((error) => {
380-
if (error.name !== 'AbortError' && onError) {
381-
onError({ nativeEvent: error.message });
382-
}
383-
});
384-
385-
// Capture a cleanup function for the current request
386-
// The reason for using a Ref is to avoid making this function a dependency
387-
// Because the change of a dependency would otherwise would re-trigger a hook
388-
cleanup.current = () => {
389-
abortCtrl.abort();
390-
setBlobUri('');
391-
URL.revokeObjectURL(uri);
392-
};
393-
}, [nextSource, onLoadStart, onError]);
394-
395-
// Run the cleanup function on unmount
396-
React.useEffect(() => cleanup.current, []);
397-
398-
const propsToPass = {
399-
...props,
400-
// Omit `onLoadStart` because we trigger it in the current scope
401-
onLoadStart: undefined,
402-
// Until the current component resolves the request (using headers)
403-
// we skip forwarding the source so the base component doesn't attempt
404-
// to load the original source
405-
source: blobUri ? { ...nextSource, uri: blobUri } : undefined
406-
};
407-
408-
return <BaseImage ref={ref} {...propsToPass} />;
409-
});
351+
Image.displayName = 'Image';
410352

411353
// $FlowIgnore: This is the correct type, but casting makes it unhappy since the variables aren't defined yet
412-
const Image: ImageComponent & ImageStatics = React.forwardRef((props, ref) => {
413-
if (props.source && props.source.headers) {
414-
return <ImageWithHeaders ref={ref} {...props} />;
415-
}
416-
417-
return <BaseImage ref={ref} {...props} />;
418-
});
419-
420-
Image.displayName = 'Image';
354+
const ImageWithStatics = (Image: React.AbstractComponent<
355+
ImageProps,
356+
React.ElementRef<typeof View>
357+
> &
358+
ImageStatics);
421359

422-
Image.getSize = function (uri, success, failure) {
360+
ImageWithStatics.getSize = function (uri, success, failure) {
423361
ImageLoader.getSize(uri, success, failure);
424362
};
425363

426-
Image.prefetch = function (uri) {
364+
ImageWithStatics.prefetch = function (uri) {
427365
return ImageLoader.prefetch(uri);
428366
};
429367

430-
Image.queryCache = function (uris) {
368+
ImageWithStatics.queryCache = function (uris) {
431369
return ImageLoader.queryCache(uris);
432370
};
433371

@@ -483,4 +421,4 @@ const resizeModeStyles = StyleSheet.create({
483421
}
484422
});
485423

486-
export default Image;
424+
export default ImageWithStatics;

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import type {
2020
TransformStyles
2121
} from '../../types/styles';
2222

23-
export type SourceObject = {
23+
type SourceObject = {
2424
/**
2525
* `body` is the HTTP body to send with the request. This must be a valid
2626
* UTF-8 string, and will be sent exactly as specified, with no
@@ -102,8 +102,8 @@ export type ImageStyle = {
102102
tintColor?: ColorValue
103103
};
104104

105-
export type ImageProps = {|
106-
...$Exact<ViewProps>,
105+
export type ImageProps = {
106+
...ViewProps,
107107
blurRadius?: number,
108108
defaultSource?: Source,
109109
draggable?: boolean,
@@ -116,4 +116,4 @@ export type ImageProps = {|
116116
resizeMode?: ResizeMode,
117117
source?: Source,
118118
style?: GenericStyleProp<ImageStyle>
119-
|};
119+
};

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ import Image from '../Image';
1616
import StyleSheet from '../StyleSheet';
1717
import View from '../View';
1818

19-
type ImageBackgroundProps = {|
19+
type ImageBackgroundProps = {
2020
...ImageProps,
2121
imageRef?: any,
2222
imageStyle?: $PropertyType<ImageProps, 'style'>,
2323
style?: $PropertyType<ViewProps, 'style'>
24-
|};
24+
};
2525

2626
const emptyObject = {};
2727

0 commit comments

Comments
 (0)