Skip to content

Commit

Permalink
Reject promise when request is aborted (#3)
Browse files Browse the repository at this point in the history
* Use FastImage

* Update readme

* Reject promise when request is aborted

* Nit for progress

* Update readme

* Update example for FastImage

* Bump package

* Update comment
  • Loading branch information
rossmartin authored Nov 9, 2022
1 parent e9f0726 commit 28b6f5f
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 37 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ startUpload({

### `abortUpload`

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

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

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).

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.
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.

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

Expand Down
1 change: 1 addition & 0 deletions example/globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module '*.png';
29 changes: 29 additions & 0 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ PODS:
- glog (0.3.5)
- hermes-engine (0.70.3)
- libevent (2.1.12)
- libwebp (1.2.3):
- libwebp/demux (= 1.2.3)
- libwebp/mux (= 1.2.3)
- libwebp/webp (= 1.2.3)
- libwebp/demux (1.2.3):
- libwebp/webp
- libwebp/mux (1.2.3):
- libwebp/demux
- libwebp/webp (1.2.3)
- OpenSSL-Universal (1.1.1100)
- RCT-Folly (2021.07.22.00):
- boost
Expand Down Expand Up @@ -370,8 +379,18 @@ PODS:
- React-jsi (= 0.70.3)
- React-logger (= 0.70.3)
- React-perflogger (= 0.70.3)
- RNFastImage (8.6.3):
- React-Core
- SDWebImage (~> 5.11.1)
- SDWebImageWebPCoder (~> 0.8.4)
- RNReactNativeHapticFeedback (1.14.0):
- React-Core
- SDWebImage (5.11.1):
- SDWebImage/Core (= 5.11.1)
- SDWebImage/Core (5.11.1)
- SDWebImageWebPCoder (0.8.5):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.10)
- SocketRocket (0.6.0)
- Yoga (1.14.0)
- YogaKit (1.18.1):
Expand Down Expand Up @@ -437,6 +456,7 @@ DEPENDENCIES:
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
- React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`)
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- RNFastImage (from `../node_modules/react-native-fast-image`)
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)

Expand All @@ -454,7 +474,10 @@ SPEC REPOS:
- FlipperKit
- fmt
- libevent
- libwebp
- OpenSSL-Universal
- SDWebImage
- SDWebImageWebPCoder
- SocketRocket
- YogaKit

Expand Down Expand Up @@ -527,6 +550,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/runtimeexecutor"
ReactCommon:
:path: "../node_modules/react-native/ReactCommon"
RNFastImage:
:path: "../node_modules/react-native-fast-image"
RNReactNativeHapticFeedback:
:path: "../node_modules/react-native-haptic-feedback"
Yoga:
Expand All @@ -551,6 +576,7 @@ SPEC CHECKSUMS:
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
hermes-engine: bb344d89a0d14c2c91ad357480a79698bb80e186
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda
RCTRequired: 5cf7e7d2f12699724b59f90350257a422eaa9492
Expand Down Expand Up @@ -580,7 +606,10 @@ SPEC CHECKSUMS:
React-RCTVibration: b9a58ffdd18446f43d493a4b0ecd603ee86be847
React-runtimeexecutor: e9b1f9310158a1e265bcdfdfd8c62d6174b947a2
ReactCommon: 01064177e66d652192c661de899b1076da962fd9
RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8
RNReactNativeHapticFeedback: 1e3efeca9628ff9876ee7cdd9edec1b336913f8c
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608
Yoga: 2ed968a4f060a92834227c036279f2736de0fce3
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
Expand Down
1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"react": "18.1.0",
"react-native": "0.70.3",
"react-native-fast-image": "8.6.3",
"react-native-haptic-feedback": "1.14.0",
"react-native-image-picker": "4.10.0",
"react-native-sortable-grid": "https://github.com/rossmartin/react-native-sortable-grid.git#b5c911c263b8c230c4973af00986724bcb234929"
Expand Down
119 changes: 85 additions & 34 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
ActivityIndicator,
Animated,
Button,
ImageBackground,
Pressable,
SafeAreaView,
StyleSheet,
Expand All @@ -15,10 +14,12 @@ import SortableGrid, { ItemOrder } from 'react-native-sortable-grid';
import ReactNativeHapticFeedback, {
HapticOptions,
} from 'react-native-haptic-feedback';
import FastImage from 'react-native-fast-image';

import ProgressBar from './components/ProgressBar';
import useFileUpload, { UploadItem } from '../../src/index';
import { allSettled } from './util/allSettled';
import useFileUpload, { UploadItem, OnProgressData } from '../../src/index';
import { allSettled, sleep } from './util/general';
import placeholderImage from './img/placeholder.png';

const hapticFeedbackOptions: HapticOptions = {
enableVibrateFallback: false,
Expand All @@ -28,7 +29,8 @@ const hapticFeedbackOptions: HapticOptions = {
interface Item extends UploadItem {
progress?: number;
failed?: boolean; // true on timeout or error
completed?: boolean; // true when request is done
completedAt?: number; // when request is done
startedAt?: number; // when request starts
}

export default function App() {
Expand All @@ -41,20 +43,17 @@ export default function App() {
// optional below
method: 'POST',
timeout: 60000, // you can set this lower to cause timeouts to happen
onProgress: ({ item, event }) => {
const progress = event?.loaded
? Math.floor((event.loaded / event.total) * 100)
: 0;
updateItem({
item,
keysAndValues: [{ key: 'progress', value: progress }],
});
},
onProgress,
onDone: (_data) => {
//console.log('onDone, data: ', data);
updateItem({
item: _data.item,
keysAndValues: [{ key: 'completed', value: true }],
keysAndValues: [
{
key: 'completedAt',
value: new Date().getTime(),
},
],
});
},
onError: (_data) => {
Expand Down Expand Up @@ -110,11 +109,47 @@ export default function App() {
});
};

async function onProgress({
item,
event,
}: {
item: Item;
event: OnProgressData['event'];
}) {
const progress = event?.loaded
? Math.round((event.loaded / event.total) * 100)
: 0;

// This logic before the else below is a hack to
// simulate progress for any that upload immediately.
// This is needed after moving to FastImage?!?!
const now = new Date().getTime();
const elapsed = now - item.startedAt!;
if (progress >= 100 && elapsed <= 200) {
for (let i = 0; i <= 100; i += 25) {
updateItem({
item,
keysAndValues: [
{
key: 'progress',
value: i,
},
],
});
await sleep(800);
}
} else {
updateItem({
item,
keysAndValues: [{ key: 'progress', value: progress }],
});
}
}

const onPressSelectMedia = async () => {
const response = await launchImageLibrary({
mediaType: 'photo',
selectionLimit: 0,
quality: 0.8,
});

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

const onPressUpload = async () => {
// allow uploading any that previously failed
setData((prevState) =>
[...prevState].map((item) => ({
...item,
failed: false,
}))
);

const promises = data
// :~)
const putItOnTheLine = async (_data: Item[]) => {
const promises = _data
.filter((item) => typeof item.progress !== 'number') // leave out any in progress
.map((item) => startUpload(item));
// use Promise.all here if you want an error from a timeout or error
const result = await allSettled(promises);
console.log('result: ', result);
};

const onPressUpload = async () => {
// allow uploading any that previously failed
setData((prevState) => {
const newState = [...prevState].map((item) => ({
...item,
failed: false,
startedAt: new Date().getTime(),
}));

putItOnTheLine(newState);

return newState;
});
};

const onPressDeleteItem = (item: Item) => () => {
setData((prevState) => {
const newState = [...prevState];
Expand All @@ -166,6 +209,10 @@ export default function App() {
key: 'failed',
value: false,
},
{
key: 'startedAt',
value: new Date().getTime(),
},
],
});
// wrapped in try/catch here just to get rid of possible unhandled promise warning
Expand Down Expand Up @@ -215,12 +262,13 @@ export default function App() {
const showProgress = !item.failed && itemProgress > 0 && itemProgress < 100;

return (
<ImageBackground
key={item.uri}
source={{ uri: item.uri }}
imageStyle={styles.image}
style={styles.imageBackground}
>
<View key={item.uri} style={styles.imageBackground}>
<FastImage
source={{ uri: item.uri }}
style={styles.image}
resizeMode={FastImage.resizeMode.cover}
defaultSource={placeholderImage}
/>
{showProgress ? (
<ProgressBar value={itemProgress} style={styles.progressBar} />
) : null}
Expand All @@ -229,11 +277,13 @@ export default function App() {
<Text style={styles.iconText}>&#x21bb;</Text>
</Pressable>
) : null}
{item.completed ? <Text style={styles.iconText}>&#10003;</Text> : null}
{item.completedAt ? (
<Text style={styles.iconText}>&#10003;</Text>
) : null}
<Pressable style={styles.deleteIcon} onPress={onPressDeleteItem(item)}>
<Text style={styles.deleteIconText}>&#x2717;</Text>
</Pressable>
</ImageBackground>
</View>
);
};

Expand Down Expand Up @@ -271,11 +321,12 @@ const styles = StyleSheet.create({
},
imageBackground: {
flex: 1,
margin: 8,
justifyContent: 'center',
alignItems: 'center',
margin: 8,
},
image: {
...StyleSheet.absoluteFillObject,
borderRadius: 12,
},
deleteIcon: {
Expand Down
Binary file added example/src/img/placeholder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export const sleep = (time: number) =>
new Promise((resolve) => setTimeout(resolve, time));

export const allSettled = (promises: Promise<any>[]) => {
return Promise.all(
promises.map((promise) =>
Expand Down
5 changes: 5 additions & 0 deletions example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3743,6 +3743,11 @@ react-native-codegen@^0.70.5:
jscodeshift "^0.13.1"
nullthrows "^1.1.1"

react-native-fast-image@8.6.3:
version "8.6.3"
resolved "https://registry.yarnpkg.com/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz#6edc3f9190092a909d636d93eecbcc54a8822255"
integrity sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==

react-native-gradle-plugin@^0.70.3:
version "0.70.3"
resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.3.tgz#cbcf0619cbfbddaa9128701aa2d7b4145f9c4fc8"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-use-file-upload",
"version": "0.1.1",
"version": "0.1.2",
"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.",
"main": "lib/commonjs/index",
"module": "lib/module/index",
Expand Down
9 changes: 9 additions & 0 deletions src/hooks/useFileUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ export default function useFileUpload({
reject(result);
};

xhr.onabort = () => {
const result: OnErrorData = {
item,
error: 'Request aborted',
};
onError?.(result);
reject(result);
};

headers?.forEach((value: string, key: string) => {
xhr.setRequestHeader(key, value);
});
Expand Down

0 comments on commit 28b6f5f

Please sign in to comment.