Skip to content

Commit ba73240

Browse files
committed
fix: catch issues when source changed
1 parent 3e5bb60 commit ba73240

File tree

4 files changed

+123
-82
lines changed

4 files changed

+123
-82
lines changed

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

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,57 @@
77
* @flow
88
*/
99

10+
import ImageLoader from '../../modules/ImageLoader';
11+
12+
type ImageSource =
13+
| string
14+
| number
15+
| {
16+
method: ?string,
17+
uri: ?string,
18+
headers: ?Object,
19+
body: ?string
20+
};
21+
1022
export default class ImageUriCache {
1123
static _maximumEntries: number = 256;
1224
static _entries = {};
1325

14-
static has(cacheId: string) {
26+
static createCacheId(source: ImageSource) {
27+
return JSON.stringify(ImageLoader.resolveSource(source));
28+
}
29+
30+
static has(source: ImageSource) {
1531
const entries = ImageUriCache._entries;
32+
const cacheId = ImageUriCache.createCacheId(source);
1633
return Boolean(entries[cacheId]);
1734
}
1835

19-
static get(cacheId: string) {
36+
static get(source: ImageSource) {
2037
const entries = ImageUriCache._entries;
38+
const cacheId = ImageUriCache.createCacheId(source);
2139
return entries[cacheId];
2240
}
2341

24-
static add(cacheId: string, displayImageUri: string) {
42+
static add(source: ImageSource, displayImageUri: ?string) {
2543
const entries = ImageUriCache._entries;
2644
const lastUsedTimestamp = Date.now();
45+
const cacheId = ImageUriCache.createCacheId(source);
2746
if (entries[cacheId]) {
2847
entries[cacheId].lastUsedTimestamp = lastUsedTimestamp;
2948
entries[cacheId].refCount += 1;
3049
} else {
3150
entries[cacheId] = {
3251
lastUsedTimestamp,
3352
refCount: 1,
34-
displayImageUri
53+
displayImageUri: displayImageUri || ImageLoader.resolveSource(source).uri
3554
};
3655
}
3756
}
3857

39-
static remove(cacheId: string) {
58+
static remove(source: ImageSource) {
4059
const entries = ImageUriCache._entries;
60+
const cacheId = ImageUriCache.createCacheId(source);
4161
if (entries[cacheId]) {
4262
entries[cacheId].refCount -= 1;
4363
}

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

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,27 @@ import React from 'react';
88
import { shallow } from 'enzyme';
99
import StyleSheet from '../../StyleSheet';
1010

11-
const originalImage = window.Image;
11+
const OriginalImage = window.Image;
1212

1313
const findImageSurfaceStyle = wrapper => StyleSheet.flatten(wrapper.childAt(0).prop('style'));
1414

15+
const createLoadEvent = uri => {
16+
const target = new OriginalImage();
17+
target.src = uri;
18+
const event = new window.Event('load');
19+
event.path = [target];
20+
21+
return event;
22+
};
23+
1524
describe('components/Image', () => {
1625
beforeEach(() => {
1726
ImageUriCache._entries = {};
1827
window.Image = jest.fn(() => ({}));
1928
});
2029

2130
afterEach(() => {
22-
window.Image = originalImage;
31+
window.Image = OriginalImage;
2332
});
2433

2534
test('prop "accessibilityLabel"', () => {
@@ -95,23 +104,24 @@ describe('components/Image', () => {
95104
describe('prop "onLoad"', () => {
96105
test('is called after image is loaded from network', () => {
97106
jest.useFakeTimers();
107+
const uri = 'https://test.com/img.jpg';
98108
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
99-
onLoad();
109+
onLoad(createLoadEvent(uri));
100110
});
101111
const onLoadStub = jest.fn();
102-
shallow(<Image onLoad={onLoadStub} source="https://test.com/img.jpg" />);
112+
shallow(<Image onLoad={onLoadStub} source={uri} />);
103113
jest.runOnlyPendingTimers();
104114
expect(ImageLoader.load).toBeCalled();
105115
expect(onLoadStub).toBeCalled();
106116
});
107117

108118
test('is called after image is loaded from cache', () => {
109119
jest.useFakeTimers();
120+
const uri = 'https://test.com/img.jpg';
110121
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
111-
onLoad();
122+
onLoad(createLoadEvent(uri));
112123
});
113124
const onLoadStub = jest.fn();
114-
const uri = 'https://test.com/img.jpg';
115125
ImageUriCache.add(uri);
116126
shallow(<Image onLoad={onLoadStub} source={uri} />);
117127
jest.runOnlyPendingTimers();
@@ -164,7 +174,7 @@ describe('components/Image', () => {
164174
test('is set immediately if the image was preloaded', () => {
165175
const uri = 'https://yahoo.com/favicon.ico';
166176
ImageLoader.load = jest.fn().mockImplementationOnce((_, onLoad, onError) => {
167-
onLoad();
177+
onLoad(createLoadEvent(uri));
168178
});
169179
return Image.prefetch(uri).then(() => {
170180
const source = { uri };
@@ -222,7 +232,7 @@ describe('components/Image', () => {
222232
});
223233
const component = shallow(<Image defaultSource={{ uri: defaultUri }} source={{ uri }} />);
224234
expect(component.find('img').prop('src')).toBe(defaultUri);
225-
loadCallback();
235+
loadCallback(createLoadEvent(uri));
226236
expect(component.find('img').prop('src')).toBe(uri);
227237
});
228238
});

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

Lines changed: 37 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import applyNativeMethods from '../../modules/applyNativeMethods';
1212
import createElement from '../createElement';
1313
import css from '../StyleSheet/css';
14-
import { getAssetByID } from '../../modules/AssetRegistry';
1514
import resolveShadowValue from '../StyleSheet/resolveShadowValue';
1615
import ImageLoader from '../../modules/ImageLoader';
1716
import ImageResizeMode from './ImageResizeMode';
@@ -33,8 +32,8 @@ const STATUS_LOADING = 'LOADING';
3332
const STATUS_PENDING = 'PENDING';
3433
const STATUS_IDLE = 'IDLE';
3534

36-
const getImageState = (uri, shouldDisplaySource) => {
37-
return shouldDisplaySource ? STATUS_LOADED : uri ? STATUS_PENDING : STATUS_IDLE;
35+
const getImageState = (source, shouldDisplaySource) => {
36+
return shouldDisplaySource ? STATUS_LOADED : source ? STATUS_PENDING : STATUS_IDLE;
3837
};
3938

4039
const resolveAssetDimensions = source => {
@@ -44,51 +43,17 @@ const resolveAssetDimensions = source => {
4443
};
4544
};
4645

47-
const svgDataUriPattern = /^(data:image\/svg\+xml;utf8,)(.*)/;
48-
const resolveAssetSource = source => {
49-
let resolvedSource = {
50-
method: 'GET',
51-
uri: '',
52-
headers: {},
53-
width: undefined,
54-
height: undefined
55-
};
56-
if (typeof source === 'number') {
57-
// get the URI from the packager
58-
const asset = getAssetByID(source);
59-
const scale = asset.scales[0];
60-
const scaleSuffix = scale !== 1 ? `@${scale}x` : '';
61-
resolvedSource.uri = asset
62-
? `${asset.httpServerLocation}/${asset.name}${scaleSuffix}.${asset.type}`
63-
: '';
64-
resolvedSource.width = asset.width;
65-
resolvedSource.height = asset.height;
66-
} else if (typeof source === 'string') {
67-
resolvedSource.uri = source;
68-
} else if (typeof source === 'object') {
69-
resolvedSource = {
70-
...resolvedSource,
71-
...source
72-
};
46+
const getCacheUrl = e => {
47+
if (e.target) {
48+
return e.target.src;
7349
}
7450

75-
if (resolvedSource.uri) {
76-
const match = resolvedSource.uri.match(svgDataUriPattern);
77-
// inline SVG markup may contain characters (e.g., #, ") that need to be escaped
78-
if (match) {
79-
const [, prefix, svg] = match;
80-
const encodedSvg = encodeURIComponent(svg);
81-
resolvedSource.uri = `${prefix}${encodedSvg}`;
82-
}
51+
// Target is not defined at this moment anymore in Chrome and thus we use path
52+
if (e.path && e.path[0]) {
53+
return e.path[0].src;
8354
}
8455

85-
return resolvedSource;
86-
};
87-
const getCacheId = source => {
88-
return JSON.stringify(resolveAssetSource(source));
89-
};
90-
const getCacheUrl = e => {
91-
return e.path && e.path[0].src;
56+
return undefined;
9257
};
9358

9459
let filterId = 0;
@@ -174,19 +139,18 @@ class Image extends Component<*, State> {
174139
constructor(props, context) {
175140
super(props, context);
176141
// If an image has been loaded before, render it immediately
177-
const cacheId = getCacheId(props.source);
178-
const resolvedSource = resolveAssetSource(props.source);
179-
const resolvedDefaultSource = resolveAssetSource(props.defaultSource);
180-
const cachedSource = ImageUriCache.get(cacheId);
142+
const resolvedSource = ImageLoader.resolveSource(props.source);
143+
const resolvedDefaultSource = ImageLoader.resolveSource(props.defaultSource);
144+
const cachedSource = ImageUriCache.get(props.source);
181145
const shouldDisplaySource = !!cachedSource;
182146
this.state = {
183147
layout: {},
184148
shouldDisplaySource,
185149
displayImageUri: shouldDisplaySource
186-
? cachedSource.uri
150+
? cachedSource.displayImageUri
187151
: resolvedDefaultSource.uri || resolvedSource.uri
188152
};
189-
this._imageState = getImageState(resolvedSource.uri, shouldDisplaySource);
153+
this._imageState = getImageState(props.source, shouldDisplaySource);
190154
this._filterId = filterId;
191155
filterId++;
192156
}
@@ -201,15 +165,16 @@ class Image extends Component<*, State> {
201165
}
202166

203167
componentDidUpdate(prevProps) {
204-
const prevCacheId = getCacheId(prevProps.source);
205-
const cacheId = getCacheId(this.props.source);
206-
const hasDefaultSource = this.props.defaultSource != null;
168+
const { defaultSource, source } = this.props;
169+
const prevCacheId = ImageUriCache.createCacheId(prevProps.source);
170+
const cacheId = ImageUriCache.createCacheId(source);
171+
const hasDefaultSource = defaultSource != null;
207172
if (prevCacheId !== cacheId) {
208-
ImageUriCache.remove(prevCacheId);
209-
const isPreviouslyLoaded = ImageUriCache.has(cacheId);
210-
isPreviouslyLoaded && ImageUriCache.add(cacheId);
211-
this._updateImageState(getImageState(cacheId, isPreviouslyLoaded), hasDefaultSource);
212-
} else if (hasDefaultSource && prevProps.defaultSource !== this.props.defaultSource) {
173+
ImageUriCache.remove(prevProps.source);
174+
const shouldDisplaySource = ImageUriCache.has(source);
175+
shouldDisplaySource && ImageUriCache.add(source);
176+
this._updateImageState(getImageState(source, shouldDisplaySource), hasDefaultSource);
177+
} else if (hasDefaultSource && prevProps.defaultSource !== defaultSource) {
213178
this._updateImageState(this._imageState, hasDefaultSource);
214179
}
215180
if (this._imageState === STATUS_PENDING) {
@@ -218,8 +183,7 @@ class Image extends Component<*, State> {
218183
}
219184

220185
componentWillUnmount() {
221-
const cacheId = getCacheId(this.props.source);
222-
ImageUriCache.remove(cacheId);
186+
ImageUriCache.remove(this.props.source);
223187
this._destroyImageLoader();
224188
this._isMounted = false;
225189
}
@@ -259,7 +223,7 @@ class Image extends Component<*, State> {
259223
}
260224
}
261225

262-
const selectedSource = resolveAssetSource(shouldDisplaySource ? source : defaultSource);
226+
const selectedSource = ImageLoader.resolveSource(shouldDisplaySource ? source : defaultSource);
263227
const imageSizeStyle = resolveAssetDimensions(selectedSource);
264228
const backgroundImage = displayImageUri ? `url("${displayImageUri}")` : null;
265229
const flatStyle = { ...StyleSheet.flatten(this.props.style) };
@@ -338,7 +302,7 @@ class Image extends Component<*, State> {
338302
const { source } = this.props;
339303
this._destroyImageLoader();
340304
this._imageRequestId = ImageLoader.load(
341-
resolveAssetSource(source),
305+
ImageLoader.resolveSource(source),
342306
this._onLoad,
343307
this._onError
344308
);
@@ -384,7 +348,7 @@ class Image extends Component<*, State> {
384348
if (onError) {
385349
onError({
386350
nativeEvent: {
387-
error: `Failed to load resource ${resolveAssetSource(source).uri} (404)`
351+
error: `Failed to load resource ${ImageLoader.resolveSource(source).uri} (404)`
388352
}
389353
});
390354
}
@@ -394,7 +358,8 @@ class Image extends Component<*, State> {
394358
_onLoad = e => {
395359
const { onLoad, source } = this.props;
396360
const event = { nativeEvent: e };
397-
ImageUriCache.add(getCacheId(source), getCacheUrl(e));
361+
362+
ImageUriCache.add(source, getCacheUrl(e));
398363
this._updateImageState(STATUS_LOADED);
399364
if (onLoad) {
400365
onLoad(event);
@@ -422,15 +387,18 @@ class Image extends Component<*, State> {
422387
};
423388

424389
_updateImageState(status: ?string, hasDefaultSource: ?boolean = false) {
425-
const { source } = this.props;
390+
const { source, defaultSource } = this.props;
391+
const resolvedSource = ImageLoader.resolveSource(defaultSource);
392+
const resolvedDefaultSource = ImageLoader.resolveSource(source);
426393
this._imageState = status;
427394
const shouldDisplaySource =
428395
this._imageState === STATUS_LOADED ||
429396
(this._imageState === STATUS_LOADING && !hasDefaultSource);
430-
const cachedId = getCacheId(source);
431-
const { displayImageUri } = ImageUriCache.has(cachedId)
432-
? ImageUriCache.get(cachedId)
433-
: this.state;
397+
const { displayImageUri } = ImageUriCache.has(source)
398+
? ImageUriCache.get(source)
399+
: {
400+
displayImageUri: resolvedSource.uri || resolvedDefaultSource.uri
401+
};
434402

435403
// only triggers a re-render when the image is loading and has no default image (to support PJPEG), loaded, or failed
436404
if (

0 commit comments

Comments
 (0)