Skip to content

Commit 28b6f5f

Browse files
authored
Reject promise when request is aborted (#3)
* Use FastImage * Update readme * Reject promise when request is aborted * Nit for progress * Update readme * Update example for FastImage * Bump package * Update comment
1 parent e9f0726 commit 28b6f5f

File tree

10 files changed

+136
-37
lines changed

10 files changed

+136
-37
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ startUpload({
6161

6262
### `abortUpload`
6363

64-
Abort a file upload for a given file.
64+
Abort a file upload for a given file. The promise from `startUpload` gets rejected and `onError` runs if present.
6565

6666
```ts
6767
// Pass the uri of a file that started uploading
@@ -199,7 +199,7 @@ useFileUpload({ headers });
199199

200200
Requests will time out if you background the app. This can be addressed by using [react-native-background-upload](https://github.com/Vydia/react-native-background-upload).
201201

202-
The React Native team did a a heavy lift to polyfill and bridge `XMLHttpRequest` to the native side for us. Hopefully some day it is updated to support requests while an app is backgrounded.
202+
The React Native team did a a heavy lift to polyfill and bridge `XMLHttpRequest` to the native side for us. [There is an open PR in React Native to allow network requests to run in the background for iOS](https://github.com/facebook/react-native/pull/31838). There are plans to have a similar PR for Android as well. `react-native-background-upload` is great but if backgrounding can be supported without any native dependencies it is a win for everyone.
203203

204204
### Why send 1 file at a time instead of multiple in a single request?
205205

example/globals.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
declare module '*.png';

example/ios/Podfile.lock

+29
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ PODS:
7575
- glog (0.3.5)
7676
- hermes-engine (0.70.3)
7777
- libevent (2.1.12)
78+
- libwebp (1.2.3):
79+
- libwebp/demux (= 1.2.3)
80+
- libwebp/mux (= 1.2.3)
81+
- libwebp/webp (= 1.2.3)
82+
- libwebp/demux (1.2.3):
83+
- libwebp/webp
84+
- libwebp/mux (1.2.3):
85+
- libwebp/demux
86+
- libwebp/webp (1.2.3)
7887
- OpenSSL-Universal (1.1.1100)
7988
- RCT-Folly (2021.07.22.00):
8089
- boost
@@ -370,8 +379,18 @@ PODS:
370379
- React-jsi (= 0.70.3)
371380
- React-logger (= 0.70.3)
372381
- React-perflogger (= 0.70.3)
382+
- RNFastImage (8.6.3):
383+
- React-Core
384+
- SDWebImage (~> 5.11.1)
385+
- SDWebImageWebPCoder (~> 0.8.4)
373386
- RNReactNativeHapticFeedback (1.14.0):
374387
- React-Core
388+
- SDWebImage (5.11.1):
389+
- SDWebImage/Core (= 5.11.1)
390+
- SDWebImage/Core (5.11.1)
391+
- SDWebImageWebPCoder (0.8.5):
392+
- libwebp (~> 1.0)
393+
- SDWebImage/Core (~> 5.10)
375394
- SocketRocket (0.6.0)
376395
- Yoga (1.14.0)
377396
- YogaKit (1.18.1):
@@ -437,6 +456,7 @@ DEPENDENCIES:
437456
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
438457
- React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`)
439458
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
459+
- RNFastImage (from `../node_modules/react-native-fast-image`)
440460
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
441461
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
442462

@@ -454,7 +474,10 @@ SPEC REPOS:
454474
- FlipperKit
455475
- fmt
456476
- libevent
477+
- libwebp
457478
- OpenSSL-Universal
479+
- SDWebImage
480+
- SDWebImageWebPCoder
458481
- SocketRocket
459482
- YogaKit
460483

@@ -527,6 +550,8 @@ EXTERNAL SOURCES:
527550
:path: "../node_modules/react-native/ReactCommon/runtimeexecutor"
528551
ReactCommon:
529552
:path: "../node_modules/react-native/ReactCommon"
553+
RNFastImage:
554+
:path: "../node_modules/react-native-fast-image"
530555
RNReactNativeHapticFeedback:
531556
:path: "../node_modules/react-native-haptic-feedback"
532557
Yoga:
@@ -551,6 +576,7 @@ SPEC CHECKSUMS:
551576
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
552577
hermes-engine: bb344d89a0d14c2c91ad357480a79698bb80e186
553578
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
579+
libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c
554580
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
555581
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
556582
RCTRequired: 5cf7e7d2f12699724b59f90350257a422eaa9492
@@ -580,7 +606,10 @@ SPEC CHECKSUMS:
580606
React-RCTVibration: b9a58ffdd18446f43d493a4b0ecd603ee86be847
581607
React-runtimeexecutor: e9b1f9310158a1e265bcdfdfd8c62d6174b947a2
582608
ReactCommon: 01064177e66d652192c661de899b1076da962fd9
609+
RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8
583610
RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c
611+
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
612+
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
584613
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
585614
Yoga: 2ed968a4f060a92834227c036279f2736de0fce3
586615
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

example/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"dependencies": {
1313
"react": "18.1.0",
1414
"react-native": "0.70.3",
15+
"react-native-fast-image": "8.6.3",
1516
"react-native-haptic-feedback": "1.14.0",
1617
"react-native-image-picker": "4.10.0",
1718
"react-native-sortable-grid": "https://github.com/rossmartin/react-native-sortable-grid.git#b5c911c263b8c230c4973af00986724bcb234929"

example/src/App.tsx

+85-34
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
ActivityIndicator,
44
Animated,
55
Button,
6-
ImageBackground,
76
Pressable,
87
SafeAreaView,
98
StyleSheet,
@@ -15,10 +14,12 @@ import SortableGrid, { ItemOrder } from 'react-native-sortable-grid';
1514
import ReactNativeHapticFeedback, {
1615
HapticOptions,
1716
} from 'react-native-haptic-feedback';
17+
import FastImage from 'react-native-fast-image';
1818

1919
import ProgressBar from './components/ProgressBar';
20-
import useFileUpload, { UploadItem } from '../../src/index';
21-
import { allSettled } from './util/allSettled';
20+
import useFileUpload, { UploadItem, OnProgressData } from '../../src/index';
21+
import { allSettled, sleep } from './util/general';
22+
import placeholderImage from './img/placeholder.png';
2223

2324
const hapticFeedbackOptions: HapticOptions = {
2425
enableVibrateFallback: false,
@@ -28,7 +29,8 @@ const hapticFeedbackOptions: HapticOptions = {
2829
interface Item extends UploadItem {
2930
progress?: number;
3031
failed?: boolean; // true on timeout or error
31-
completed?: boolean; // true when request is done
32+
completedAt?: number; // when request is done
33+
startedAt?: number; // when request starts
3234
}
3335

3436
export default function App() {
@@ -41,20 +43,17 @@ export default function App() {
4143
// optional below
4244
method: 'POST',
4345
timeout: 60000, // you can set this lower to cause timeouts to happen
44-
onProgress: ({ item, event }) => {
45-
const progress = event?.loaded
46-
? Math.floor((event.loaded / event.total) * 100)
47-
: 0;
48-
updateItem({
49-
item,
50-
keysAndValues: [{ key: 'progress', value: progress }],
51-
});
52-
},
46+
onProgress,
5347
onDone: (_data) => {
5448
//console.log('onDone, data: ', data);
5549
updateItem({
5650
item: _data.item,
57-
keysAndValues: [{ key: 'completed', value: true }],
51+
keysAndValues: [
52+
{
53+
key: 'completedAt',
54+
value: new Date().getTime(),
55+
},
56+
],
5857
});
5958
},
6059
onError: (_data) => {
@@ -110,11 +109,47 @@ export default function App() {
110109
});
111110
};
112111

112+
async function onProgress({
113+
item,
114+
event,
115+
}: {
116+
item: Item;
117+
event: OnProgressData['event'];
118+
}) {
119+
const progress = event?.loaded
120+
? Math.round((event.loaded / event.total) * 100)
121+
: 0;
122+
123+
// This logic before the else below is a hack to
124+
// simulate progress for any that upload immediately.
125+
// This is needed after moving to FastImage?!?!
126+
const now = new Date().getTime();
127+
const elapsed = now - item.startedAt!;
128+
if (progress >= 100 && elapsed <= 200) {
129+
for (let i = 0; i <= 100; i += 25) {
130+
updateItem({
131+
item,
132+
keysAndValues: [
133+
{
134+
key: 'progress',
135+
value: i,
136+
},
137+
],
138+
});
139+
await sleep(800);
140+
}
141+
} else {
142+
updateItem({
143+
item,
144+
keysAndValues: [{ key: 'progress', value: progress }],
145+
});
146+
}
147+
}
148+
113149
const onPressSelectMedia = async () => {
114150
const response = await launchImageLibrary({
115151
mediaType: 'photo',
116152
selectionLimit: 0,
117-
quality: 0.8,
118153
});
119154

120155
const items: Item[] =
@@ -127,23 +162,31 @@ export default function App() {
127162
setData((prevState) => [...prevState, ...items]);
128163
};
129164

130-
const onPressUpload = async () => {
131-
// allow uploading any that previously failed
132-
setData((prevState) =>
133-
[...prevState].map((item) => ({
134-
...item,
135-
failed: false,
136-
}))
137-
);
138-
139-
const promises = data
165+
// :~)
166+
const putItOnTheLine = async (_data: Item[]) => {
167+
const promises = _data
140168
.filter((item) => typeof item.progress !== 'number') // leave out any in progress
141169
.map((item) => startUpload(item));
142170
// use Promise.all here if you want an error from a timeout or error
143171
const result = await allSettled(promises);
144172
console.log('result: ', result);
145173
};
146174

175+
const onPressUpload = async () => {
176+
// allow uploading any that previously failed
177+
setData((prevState) => {
178+
const newState = [...prevState].map((item) => ({
179+
...item,
180+
failed: false,
181+
startedAt: new Date().getTime(),
182+
}));
183+
184+
putItOnTheLine(newState);
185+
186+
return newState;
187+
});
188+
};
189+
147190
const onPressDeleteItem = (item: Item) => () => {
148191
setData((prevState) => {
149192
const newState = [...prevState];
@@ -166,6 +209,10 @@ export default function App() {
166209
key: 'failed',
167210
value: false,
168211
},
212+
{
213+
key: 'startedAt',
214+
value: new Date().getTime(),
215+
},
169216
],
170217
});
171218
// wrapped in try/catch here just to get rid of possible unhandled promise warning
@@ -215,12 +262,13 @@ export default function App() {
215262
const showProgress = !item.failed && itemProgress > 0 && itemProgress < 100;
216263

217264
return (
218-
<ImageBackground
219-
key={item.uri}
220-
source={{ uri: item.uri }}
221-
imageStyle={styles.image}
222-
style={styles.imageBackground}
223-
>
265+
<View key={item.uri} style={styles.imageBackground}>
266+
<FastImage
267+
source={{ uri: item.uri }}
268+
style={styles.image}
269+
resizeMode={FastImage.resizeMode.cover}
270+
defaultSource={placeholderImage}
271+
/>
224272
{showProgress ? (
225273
<ProgressBar value={itemProgress} style={styles.progressBar} />
226274
) : null}
@@ -229,11 +277,13 @@ export default function App() {
229277
<Text style={styles.iconText}>&#x21bb;</Text>
230278
</Pressable>
231279
) : null}
232-
{item.completed ? <Text style={styles.iconText}>&#10003;</Text> : null}
280+
{item.completedAt ? (
281+
<Text style={styles.iconText}>&#10003;</Text>
282+
) : null}
233283
<Pressable style={styles.deleteIcon} onPress={onPressDeleteItem(item)}>
234284
<Text style={styles.deleteIconText}>&#x2717;</Text>
235285
</Pressable>
236-
</ImageBackground>
286+
</View>
237287
);
238288
};
239289

@@ -271,11 +321,12 @@ const styles = StyleSheet.create({
271321
},
272322
imageBackground: {
273323
flex: 1,
324+
margin: 8,
274325
justifyContent: 'center',
275326
alignItems: 'center',
276-
margin: 8,
277327
},
278328
image: {
329+
...StyleSheet.absoluteFillObject,
279330
borderRadius: 12,
280331
},
281332
deleteIcon: {

example/src/img/placeholder.png

1.73 KB
Loading

example/src/util/allSettled.ts renamed to example/src/util/general.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
export const sleep = (time: number) =>
2+
new Promise((resolve) => setTimeout(resolve, time));
3+
14
export const allSettled = (promises: Promise<any>[]) => {
25
return Promise.all(
36
promises.map((promise) =>

example/yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -3743,6 +3743,11 @@ react-native-codegen@^0.70.5:
37433743
jscodeshift "^0.13.1"
37443744
nullthrows "^1.1.1"
37453745

3746+
3747+
version "8.6.3"
3748+
resolved "https://registry.yarnpkg.com/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz#6edc3f9190092a909d636d93eecbcc54a8822255"
3749+
integrity sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==
3750+
37463751
react-native-gradle-plugin@^0.70.3:
37473752
version "0.70.3"
37483753
resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.3.tgz#cbcf0619cbfbddaa9128701aa2d7b4145f9c4fc8"

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-use-file-upload",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"description": "A hook for uploading files using multipart form data with React Native. Provides a simple way to track upload progress, abort an upload, and handle timeouts. Written in TypeScript and no dependencies required.",
55
"main": "lib/commonjs/index",
66
"module": "lib/module/index",

src/hooks/useFileUpload.ts

+9
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ export default function useFileUpload({
7070
reject(result);
7171
};
7272

73+
xhr.onabort = () => {
74+
const result: OnErrorData = {
75+
item,
76+
error: 'Request aborted',
77+
};
78+
onError?.(result);
79+
reject(result);
80+
};
81+
7382
headers?.forEach((value: string, key: string) => {
7483
xhr.setRequestHeader(key, value);
7584
});

0 commit comments

Comments
 (0)