Skip to content

Commit 1f2aefa

Browse files
kidrocanecolas
authored andcommitted
[add] Image source headers handling
Extend ImageLoader functionality to be able to work with image sources containing headers We preserve the existing strategy that works with image.src for cases where source is just an uri with no headers When sources contain headers we make a fetch request and then render a local url for the downloaded blob (URL.createObjectURL) Fix #1019 Close #2442
1 parent 4a61c16 commit 1f2aefa

File tree

5 files changed

+253
-42
lines changed

5 files changed

+253
-42
lines changed

packages/react-native-web-examples/pages/image/index.js

+23
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ const dataBase64Svg =
1515
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMjAwJyBoZWlnaHQ9JzIwMCcgZmlsbD0iIzAwMDAwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDEwMCAxMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGQ9Ik0yNS44NjcsNDguODUzQzMyLjgwNiw1MC4xNzYsNDYuNDYsNTIuNSw2MS4yMTUsNTIuNWgwLjAwNWM5LjcxLDAsMTguNDAxLTEuMDU3LDI1LjkzOC0yLjkxMyAgIGMwLjE1OS0wLjA0NiwwLjM1LTAuMTM1LDAuNTY1LTAuMTg3YzAuMjgyLTAuMDcyLDAuNTY1LTAuMTY0LDAuODQ0LTAuMjM4YzMuMTg0LTAuOTY0LDIuNTc3LTMuMDUxLDIuMTk5LTMuODUyICAgYy00LjE2Ni03LjcxOS0xNS4wODYtMjMuNDE1LTM1LjAyOC0yMy40MTVjLTIyLjE2OSwwLTMwLjI2MiwxMC42MzUtMzMuMTQsMTkuNTg5QzIyLjU0NSw0Mi4zMzMsMjIuNDA3LDQ3LjEzNSwyNS44NjcsNDguODUzeiAgICBNMjguNjc2LDM4LjAzMmMwLjAxMy0wLjAzNiwwLjYxNC0xLjYyNiwxLjkyMy0xLjAwOGMxLjEzMywwLjUzNSwwLjk2MSwxLjU2MywwLjg4NywxLjg1Yy0wLjAwNywwLjAyNC0wLjAxNCwwLjA0OC0wLjAyMSwwLjA3MyAgIGMwLDAuMDAxLTAuMDAxLDAuMDA0LTAuMDAxLDAuMDA0bDAsMGMtMC4yNDksMC45MjktMC40MDQsMi4wODYtMC4wMTcsMi44NmMwLjE2LDAuMzE5LDAuNDkyLDAuNzY4LDEuNTQyLDAuOTg3bDAuMzY2LDAuMDc3ICAgYzIwLjgxNiw0LjM2LDM2LDIuOTMzLDQ1LjY3OCwwLjYyNmwtMC4wMDQsMC4wMDJjMCwwLDAuMDA1LTAuMDAyLDAuMDA3LTAuMDAzYzAuMjEyLTAuMDUsMC40MjEtMC4xMDEsMC42MjgtMC4xNTIgICBjMC41MDktMC4wNSwxLjE3MywwLjA3OCwxLjM5OSwxYzAuMzUxLDEuNDI0LTAuOTczLDEuODk1LTEuMjE3LDEuOTY5Yy01LjMyNSwxLjI3OS0xMi4yNjYsMi4zMDYtMjAuODM1LDIuMzA3ICAgYy03LjUwNSwwLTE2LjI1NS0wLjc4Ny0yNi4yNTctMi44ODJsLTAuMzY0LTAuMDc3Yy0yLjEyLTAuNDQyLTMuMTExLTEuNjMzLTMuNTY5LTIuNTU1QzI3Ljk4NSw0MS40MjEsMjguMjgxLDM5LjQxNiwyOC42NzYsMzguMDMyICAgeiI+PC9wYXRoPjxjaXJjbGUgY3g9IjEwLjQ5MyIgY3k9IjIzLjQ1NSIgcj0iMC42MTkiPjwvY2lyY2xlPjxwYXRoIGQ9Ik0yLjA4LDI4LjMwOGMwLjY3Ni0wLjE3OCwwLjk4My0wLjM1MiwxLjE3NC0wLjVDNC42OSwyNi42OSw2LjUsMjcuNDgzLDcuNSwyOC4zNTd2MC4wMDJjMCwwLDEuNzExLDEuMjM1LDAuNzM3LDIuMjAyICAgYy0wLjk3NCwwLjk2NS0yLjMxOSwwLjAwNi0yLjMxOSwwLjAwNmwwLjAzNSwwLjAxNmMtMC4zMjctMC4yMDMtMC42LTAuNTYxLTAuNzgtMC41ODRjLTAuMzcsMC4yNi0wLjg3NiwwLjUtMS40NzYsMC41SDMuNyAgIGMwLDAtMS4zNDUsMC43MDksMC4xNzgsMS42NTJjMC4wMDEsMC4wMDEsMC4wMDIsMC4wNzIsMC4wMDQsMC4wNzNjMy45MzksMi4zNDIsOC4yNzEsNS43MDEsOC4yNzEsOC44OCAgIGMwLDAuNjkxLDAuMiwxNy4wNDIsMTcuNjI2LDI0LjczOWwwLjk2NywwLjQ0MmwtMC4xLDEuMDU5Yy0wLjQyMSw0LjM5LDEuMTQ1LDEwLjE5MSwxMC45OTMsMTIuODg4bDAuMTEzLDAuMDM4ICAgYzAuMDY3LDAuMDIzLDYuNzMyLDIuNDI5LDEwLjkwNywyLjQyOWMxLjU4NCwwLDIuMTU1LTAuMzUyLDIuMjQzLTAuNTYxYzAuMDg1LTAuMjAyLDAuNjEyLTIuMTY0LTYuMzMyLTkuMzg3bDAuMDAyLTAuMTgzICAgYzAsMC0yLjQ3Ny0zLjA3LDEuNTMzLTMuMDdjMC4wMSwwLDAuMDE5LDAsMC4wMjksMGMxLjI4NSwwLDIuNjA4LDAuMjE1LDMuOTgsMC4xODRjNC43NzEtMC4xMTcsOS4zMTYtMC40MjUsMTMuNTA2LTEuMDk2ICAgbDAuNDc0LTAuMDI4bDAuNjY4LDAuMTU4YzkuNjUxLDQuOTQ4LDE2LjczOCw3LjcxNiwxOS43MzgsNy43MTZ2MC4wMDZjMCwwLDAuMTY0LDAuMDExLDAuMjMsMC4wMDQgICBjLTAuMTg5LTAuNzIzLTIuMjMtMi44LTcuMjMtOS4wNzl2MC4wMjFjMCwwLTEuNTEyLTEuNjU4LDAuNzk3LTIuNjUzYzAuMDYzLTAuMDI2LDAuMDA4LDAuMDIzLDAuMDYtMC4wMDEgICBjOC42MzktMy41MDksMTMuNTAxLTguMjA0LDE1LjQxMS0xMS43NzVjMS4xNDUtMi4xMjksMC4yMDYtMi43ODQtMC42NTktMi45NzZjLTAuMzE3LTAuMDM4LTAuNjM0LTAuMDYyLTAuOTEyLTAuMDYyICAgYy0wLjIwNSwwLTAuMzc5LDAuMDEtMC41MjgsMC4wMjdsLTMuMTQzLDEuMjE0QzgzLjczMiw1My45MjYsNzMuMjE4LDU1LjUsNjEuMjIsNTUuNWMtMC4wMDIsMC0wLjAwNSwwLTAuMDA1LDAgICBjLTE1LjEyOCwwLTI5LjEwMS0yLjQzMi0zNi4wODMtMy43NzFsLTAuMTczLTAuMTExbC0wLjE2LTAuMTI2Yy01Ljg1OC0yLjY4MS01LjEzNy0xMC4yMDItNS4xMDMtMTAuNTE5bDAuMDYtMC4zICAgYzAuODk1LTIuODM4LDIuNDY3LTYuMzUyLDUuMjEzLTkuNzE5Yy0xLjgwOC0xLjM2OS00LjU5LTQuMTg4LTQuNDMtOC40OTRjMC4wNDYtMS4yNDQtMC40ODYtMi41MDgtMS40OTgtMy41NTkgICBjLTEuNDk4LTEuNTU1LTMuNzg1LTIuNDQ2LTYuMjc0LTIuNDQ2Yy0xLjc3LDAtMy41NTMsMC40NDItNS4yOTMsMS4zMTRjLTQuMDYxLDIuMDM1LTQuODU1LDQuNzM2LTUuNjkyLDcuNTk2ICAgYy0wLjEzNiwwLjQ2OC0wLjI4NCwwLjkzOS0wLjQzOCwxLjQxYy0wLjAwNiwwLjAxOS0wLjAyMiwwLjAzNS0wLjAyOCwwLjA1NkMwLjgzMywyOC40MjMsMS42OTEsMjguMzksMi4wOCwyOC4zMDh6IE0xMC40OTMsMTkuOTA4ICAgYzEuOTU2LDAsMy41NDgsMS41OTEsMy41NDgsMy41NDdjMCwxLjk1Ny0xLjU5MiwzLjU0OC0zLjU0OCwzLjU0OGMtMS45NTcsMC0zLjU0OC0xLjU5Mi0zLjU0OC0zLjU0OCAgIEM2Ljk0NCwyMS40OTksOC41MzYsMTkuOTA4LDEwLjQ5MywxOS45MDh6Ij48L3BhdGg+PC9nPjwvc3ZnPg==';
1616
const dataSvg =
1717
'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>';
18+
const sourceWithHeaders = {
19+
uri: placeholder,
20+
headers: {
21+
'x-token': '0012345'
22+
}
23+
};
24+
const sourceWithHeadersAndRedirect = {
25+
uri: source,
26+
headers: {
27+
'x-token': '0012345'
28+
}
29+
};
1830

1931
function Divider() {
2032
return <View style={styles.divider} />;
@@ -114,6 +126,17 @@ export default function ImagePage() {
114126
/>
115127
</View>
116128
</View>
129+
<Divider />
130+
<View style={styles.row}>
131+
<View style={styles.column}>
132+
<Text style={[styles.text]}>With Headers</Text>
133+
<Image source={sourceWithHeaders} style={styles.image} />
134+
</View>
135+
<View style={styles.column}>
136+
<Text style={[styles.text]}>Headers & Redirect</Text>
137+
<Image source={sourceWithHeadersAndRedirect} style={styles.image} />
138+
</View>
139+
</View>
117140
</Example>
118141
);
119142
}

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -329,14 +329,14 @@ exports[`components/Image prop "style" removes other unsupported View styles 1`]
329329
>
330330
<div
331331
class="css-view-175oi2r r-backgroundColor-1niwhzg r-backgroundPosition-vvn4in r-backgroundRepeat-u6sd8q r-bottom-1p0dtai r-height-1pi2tsx r-left-1d2f490 r-position-u8s1d r-right-zchlnj r-top-ipm5af r-width-13qz1uu r-zIndex-1wyyakw r-backgroundSize-4gszlv"
332-
style="filter: url(#tint-55);"
332+
style="filter: url(#tint-66);"
333333
/>
334334
<svg
335335
style="position: absolute; height: 0px; visibility: hidden; width: 0px;"
336336
>
337337
<defs>
338338
<filter
339-
id="tint-55"
339+
id="tint-66"
340340
>
341341
<feflood
342342
flood-color="blue"
@@ -379,7 +379,7 @@ exports[`components/Image prop "tintColor" convert to filter 1`] = `
379379
>
380380
<div
381381
class="css-view-175oi2r r-backgroundColor-1niwhzg r-backgroundPosition-vvn4in r-backgroundRepeat-u6sd8q r-bottom-1p0dtai r-height-1pi2tsx r-left-1d2f490 r-position-u8s1d r-right-zchlnj r-top-ipm5af r-width-13qz1uu r-zIndex-1wyyakw r-backgroundSize-4gszlv"
382-
style="background-image: url(https://google.com/favicon.ico); filter: url(#tint-56);"
382+
style="background-image: url(https://google.com/favicon.ico); filter: url(#tint-67);"
383383
/>
384384
<img
385385
alt=""
@@ -392,7 +392,7 @@ exports[`components/Image prop "tintColor" convert to filter 1`] = `
392392
>
393393
<defs>
394394
<filter
395-
id="tint-56"
395+
id="tint-67"
396396
>
397397
<feflood
398398
flood-color="red"

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

+86-17
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,24 @@ import Image from '../';
1212
import ImageLoader, { ImageUriCache } from '../../../modules/ImageLoader';
1313
import PixelRatio from '../../PixelRatio';
1414
import React from 'react';
15-
import { act, render } from '@testing-library/react';
15+
import { act, render, waitFor } from '@testing-library/react';
1616

1717
const originalImage = window.Image;
1818

1919
describe('components/Image', () => {
2020
beforeEach(() => {
2121
ImageUriCache._entries = {};
2222
window.Image = jest.fn(() => ({}));
23+
ImageLoader.load = jest
24+
.fn()
25+
.mockImplementation((source, onLoad, onError) => {
26+
act(() => onLoad({ source }));
27+
});
28+
ImageLoader.loadWithHeaders = jest.fn().mockImplementation((source) => ({
29+
source,
30+
promise: Promise.resolve(`blob:${Math.random()}`),
31+
cancel: jest.fn()
32+
}));
2333
});
2434

2535
afterEach(() => {
@@ -102,10 +112,6 @@ describe('components/Image', () => {
102112

103113
describe('prop "onLoad"', () => {
104114
test('is called after image is loaded from network', () => {
105-
jest.useFakeTimers();
106-
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
107-
onLoad();
108-
});
109115
const onLoadStartStub = jest.fn();
110116
const onLoadStub = jest.fn();
111117
const onLoadEndStub = jest.fn();
@@ -117,15 +123,10 @@ describe('components/Image', () => {
117123
source="https://test.com/img.jpg"
118124
/>
119125
);
120-
jest.runOnlyPendingTimers();
121126
expect(onLoadStub).toBeCalled();
122127
});
123128

124129
test('is called after image is loaded from cache', () => {
125-
jest.useFakeTimers();
126-
ImageLoader.load = jest.fn().mockImplementation((_, onLoad, onError) => {
127-
onLoad();
128-
});
129130
const onLoadStartStub = jest.fn();
130131
const onLoadStub = jest.fn();
131132
const onLoadEndStub = jest.fn();
@@ -139,7 +140,6 @@ describe('components/Image', () => {
139140
source={uri}
140141
/>
141142
);
142-
jest.runOnlyPendingTimers();
143143
expect(onLoadStub).toBeCalled();
144144
ImageUriCache.remove(uri);
145145
});
@@ -223,6 +223,34 @@ describe('components/Image', () => {
223223
});
224224
});
225225

226+
describe('prop "onLoadStart"', () => {
227+
test('is called on update if "headers" are modified', () => {
228+
const onLoadStartStub = jest.fn();
229+
const { rerender } = render(
230+
<Image
231+
onLoadStart={onLoadStartStub}
232+
source={{
233+
uri: 'https://test.com/img.jpg',
234+
headers: { 'x-custom-header': 'abc123' }
235+
}}
236+
/>
237+
);
238+
act(() => {
239+
rerender(
240+
<Image
241+
onLoadStart={onLoadStartStub}
242+
source={{
243+
uri: 'https://test.com/img.jpg',
244+
headers: { 'x-custom-header': '123abc' }
245+
}}
246+
/>
247+
);
248+
});
249+
250+
expect(onLoadStartStub.mock.calls.length).toBe(2);
251+
});
252+
});
253+
226254
describe('prop "resizeMode"', () => {
227255
['contain', 'cover', 'none', 'repeat', 'stretch', undefined].forEach(
228256
(resizeMode) => {
@@ -241,7 +269,8 @@ describe('components/Image', () => {
241269
'',
242270
{},
243271
{ uri: '' },
244-
{ uri: 'https://google.com' }
272+
{ uri: 'https://google.com' },
273+
{ uri: 'https://google.com', headers: { 'x-custom-header': 'abc123' } }
245274
];
246275
sources.forEach((source) => {
247276
expect(() => render(<Image source={source} />)).not.toThrow();
@@ -257,11 +286,6 @@ describe('components/Image', () => {
257286

258287
test('is set immediately if the image was preloaded', () => {
259288
const uri = 'https://yahoo.com/favicon.ico';
260-
ImageLoader.load = jest
261-
.fn()
262-
.mockImplementationOnce((_, onLoad, onError) => {
263-
onLoad();
264-
});
265289
return Image.prefetch(uri).then(() => {
266290
const source = { uri };
267291
const { container } = render(<Image source={source} />, {
@@ -342,6 +366,51 @@ describe('components/Image', () => {
342366
'http://localhost/static/[email protected]'
343367
);
344368
});
369+
370+
test('it works with headers in 2 stages', async () => {
371+
const uri = 'https://google.com/favicon.ico';
372+
const headers = { 'x-custom-header': 'abc123' };
373+
const source = { uri, headers };
374+
375+
// Stage 1
376+
const loadRequest = {
377+
promise: Promise.resolve('blob:123'),
378+
cancel: jest.fn(),
379+
source
380+
};
381+
382+
ImageLoader.loadWithHeaders.mockReturnValue(loadRequest);
383+
384+
render(<Image source={source} />);
385+
386+
expect(ImageLoader.loadWithHeaders).toHaveBeenCalledWith(
387+
expect.objectContaining(source)
388+
);
389+
390+
// Stage 2
391+
return waitFor(() => {
392+
expect(ImageLoader.load).toHaveBeenCalledWith(
393+
'blob:123',
394+
expect.any(Function),
395+
expect.any(Function)
396+
);
397+
});
398+
});
399+
400+
// A common case is `source` declared as an inline object, which cause is to be a
401+
// new object (with the same content) each time parent component renders
402+
test('it still loads the image if source object is changed', () => {
403+
const uri = 'https://google.com/favicon.ico';
404+
const headers = { 'x-custom-header': 'abc123' };
405+
const { rerender } = render(<Image source={{ uri, headers }} />);
406+
rerender(<Image source={{ uri, headers }} />);
407+
408+
// when the underlying source didn't change we don't expect more than 1 load calls
409+
return waitFor(() => {
410+
expect(ImageLoader.loadWithHeaders).toHaveBeenCalledTimes(1);
411+
expect(ImageLoader.load).toHaveBeenCalledTimes(1);
412+
});
413+
});
345414
});
346415

347416
describe('prop "style"', () => {

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

+85-19
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* @flow
99
*/
1010

11+
import type { ImageSource, LoadRequest } from '../../modules/ImageLoader';
1112
import type { ImageProps } from './types';
1213

1314
import * as React from 'react';
@@ -165,6 +166,23 @@ function resolveAssetUri(source): ?string {
165166
return uri;
166167
}
167168

169+
function raiseOnErrorEvent(uri, { onError, onLoadEnd }) {
170+
if (onError) {
171+
onError({
172+
nativeEvent: {
173+
error: `Failed to load resource ${uri} (404)`
174+
}
175+
});
176+
}
177+
if (onLoadEnd) onLoadEnd();
178+
}
179+
180+
function hasSourceDiff(a: ImageSource, b: ImageSource) {
181+
return (
182+
a.uri !== b.uri || JSON.stringify(a.headers) !== JSON.stringify(b.headers)
183+
);
184+
}
185+
168186
interface ImageStatics {
169187
getSize: (
170188
uri: string,
@@ -177,10 +195,12 @@ interface ImageStatics {
177195
) => Promise<{| [uri: string]: 'disk/memory' |}>;
178196
}
179197

180-
const Image: React.AbstractComponent<
198+
type ImageComponent = React.AbstractComponent<
181199
ImageProps,
182200
React.ElementRef<typeof View>
183-
> = React.forwardRef((props, ref) => {
201+
>;
202+
203+
const BaseImage: ImageComponent = React.forwardRef((props, ref) => {
184204
const {
185205
'aria-label': ariaLabel,
186206
blurRadius,
@@ -300,16 +320,7 @@ const Image: React.AbstractComponent<
300320
},
301321
function error() {
302322
updateState(ERRORED);
303-
if (onError) {
304-
onError({
305-
nativeEvent: {
306-
error: `Failed to load resource ${uri} (404)`
307-
}
308-
});
309-
}
310-
if (onLoadEnd) {
311-
onLoadEnd();
312-
}
323+
raiseOnErrorEvent(uri, { onError, onLoadEnd });
313324
}
314325
);
315326
}
@@ -353,14 +364,69 @@ const Image: React.AbstractComponent<
353364
);
354365
});
355366

356-
Image.displayName = 'Image';
367+
BaseImage.displayName = 'Image';
357368

358-
// $FlowIgnore: This is the correct type, but casting makes it unhappy since the variables aren't defined yet
359-
const ImageWithStatics = (Image: React.AbstractComponent<
360-
ImageProps,
361-
React.ElementRef<typeof View>
362-
> &
363-
ImageStatics);
369+
/**
370+
* This component handles specifically loading an image source with headers
371+
* default source is never loaded using headers
372+
*/
373+
const ImageWithHeaders: ImageComponent = React.forwardRef((props, ref) => {
374+
// $FlowIgnore: This component would only be rendered when `source` matches `ImageSource`
375+
const nextSource: ImageSource = props.source;
376+
const [blobUri, setBlobUri] = React.useState('');
377+
const request = React.useRef<LoadRequest>({
378+
cancel: () => {},
379+
source: { uri: '', headers: {} },
380+
promise: Promise.resolve('')
381+
});
382+
383+
const { onLoadStart, ...forwardedProps } = props;
384+
const { onError, onLoadEnd } = forwardedProps;
385+
386+
React.useEffect(() => {
387+
if (!hasSourceDiff(nextSource, request.current.source)) {
388+
return;
389+
}
390+
391+
// When source changes we want to clean up any old/running requests
392+
request.current.cancel();
393+
394+
if (onLoadStart) {
395+
onLoadStart();
396+
}
397+
398+
// Store a ref for the current load request so we know what's the last loaded source,
399+
// and so we can cancel it if a different source is passed through props
400+
request.current = ImageLoader.loadWithHeaders(nextSource);
401+
402+
request.current.promise
403+
.then((uri) => setBlobUri(uri))
404+
.catch(() =>
405+
raiseOnErrorEvent(request.current.source.uri, { onError, onLoadEnd })
406+
);
407+
}, [nextSource, onLoadStart, onError, onLoadEnd]);
408+
409+
// Cancel any request on unmount
410+
React.useEffect(() => request.current.cancel, []);
411+
412+
// Until the current component resolves the request (using headers)
413+
// we skip forwarding the source so the base component doesn't attempt
414+
// to load the original source
415+
const source = blobUri ? { ...nextSource, uri: blobUri } : undefined;
416+
417+
return <BaseImage {...forwardedProps} ref={ref} source={source} />;
418+
});
419+
420+
// $FlowFixMe
421+
const ImageWithStatics: ImageComponent & ImageStatics = React.forwardRef(
422+
(props, ref) => {
423+
if (props.source && props.source.headers) {
424+
return <ImageWithHeaders {...props} ref={ref} />;
425+
}
426+
427+
return <BaseImage {...props} ref={ref} />;
428+
}
429+
);
364430

365431
ImageWithStatics.getSize = function (uri, success, failure) {
366432
ImageLoader.getSize(uri, success, failure);

0 commit comments

Comments
 (0)