Hello Everyone
I am using React Native
and I wanna adding some feature where the user can import multiple files to my app and the user can cancel the import progress at any time user want
But, when the user import these files I want to import them one by one to tell the user which files have been successfully imported and which files have not been imported,
it's important for me because I want to tell the user how many files that have been selected are successfully imported and also is useful for displaying each file to the UI when that file in progress importing and this requires me to use a Recursive Functions
,
The Problem is I have no idea how to cancel a Promise
that use a Recursive Functions
, I try with makeCancelable
method from react site it doesn't work and I think it just only cancel the Promise
at top of the tree Recursive Functions
, not all executed Promise. Also, I don't wanna use any deps/packages if it's possible. Any Idea?
Core Tools
Using real device Xiaomi Redmi 1S 4.4 Kitkat
"react": "16.13.1",
"react-native": "0.63.3",
Code Sample
importFiles.js
import RNFetchBlob from 'rn-fetch-blob';
import CameraRoll from '@react-native-community/cameraroll';
import _ from 'lodash';
const fs = RNFetchBlob.fs;
/**
* Import directory destination
*/
const dest = `${fs.dirs.SDCardDir}/VEGA/.src/`;
/**
* An increment index to tell the function which index to run
*/
let i = 0;
/**
* Import the files to this App with some encryption
* @param {object} config
* @param {string} config.albumId
* @param {[{
* uri: string,
* mimeType: string,
* albumName: string,
* timestamp: number,
* isSelected: boolean,
* }]} config.files
* @param {'fake' | 'real'=} config.encryptionMode
*/
const importFiles = config => {
return new Promise(async (resolve, reject) => {
const {albumId, files, encryptionMode} = config;
if (_.isEmpty(files) || !_.isArray(files)) {
reject('invalid files');
return;
}
const file = files[i];
/**
* It's mean Done when the file got "undefined"
*/
if (!file) {
resolve();
return;
}
const uri = file.uri.replace('file://', '');
try {
/**
* Fake Encryption
*
* It's fast but not totally secure
*/
if (!encryptionMode || encryptionMode === 'fake') {
const md5 = await fs.hash(uri, 'md5');
const importedFileUri = `${dest}.${md5}.xml`;
/**
* TODO:
* * Test cancelable
*/
await fs.readFile(uri, 'base64');
// await fs.mv(uri, importedFileUri);
// await CameraRoll.deletePhotos([uri]);
/**
* If successfully import this file then continue it to
* the next index until it's "undefined"
*/
i++;
}
/**
* Real Encryption
*
* It's slow but totally secure
*/
if (encryptionMode === 'real') {
}
await importFiles({files, encryptionMode}).promise;
resolve();
} catch (error) {
reject(error);
}
});
};
export default importFiles;
FileImporter.js (How I use makeCancelable
method)
import React, {useEffect} from 'react';
import {View, Alert} from 'react-native';
import {Contrainer, TopNavigation, Text} from '../components/Helper';
import {connect} from 'react-redux';
import utils from '../utils';
const makeCancelable = promise => {
let hasCanceled_ = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then(
val => (hasCanceled_ ? reject({isCanceled: true}) : resolve(val)),
error => (hasCanceled_ ? reject({isCanceled: true}) : reject(error)),
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};
const FileImporter = props => {
const {userGalleryFiles} = props;
useEffect(() => {
props.navigation.addListener('beforeRemove', e => {
e.preventDefault();
Alert.alert(
'Cancel?',
'Are you sure want to cancel this?',
[
{text: 'No', onPress: () => {}},
{
text: 'Yes!',
onPress: () => props.navigation.dispatch(e.data.action),
},
],
{cancelable: true},
);
});
(async () => {
const selectedFiles = userGalleryFiles.filter(
file => file.isSelected === true,
);
try {
await makeCancelable(utils.importFiles({files: selectedFiles})).promise;
console.warn('Oh God!!!');
} catch (error) {
console.error(error);
}
return () => makeCancelable().cancel();
})();
}, []);
return (
<Contrainer>
<TopNavigation title='Importing files...' disableIconLeft />
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Text hint>0 / 20</Text>
</View>
</Contrainer>
);
};
const mapStateToProps = ({userGalleryFiles}) => ({userGalleryFiles});
export default connect(mapStateToProps)(FileImporter);
Expected Results
importFiles.js
can be canceled when FileImporter.js
is unmounted
Actual Results
importFiles.js
still running even FileImporter.js
is unmounted
Try React useEffect({}, [i])
with deps
on it instead using Recursive Functions
import React, {useEffect, useState} from 'react';
import {View, Alert} from 'react-native';
import {Contrainer, TopNavigation, Text} from '../components/Helper';
import {connect} from 'react-redux';
import utils from '../utils';
const FileImporter = props => {
const {userGalleryFiles} = props;
const [currentIndexWantToImport, setCurrentIndexWantToImport] = useState(0)
useEffect(() => {
(async () => {
const selectedFiles = userGalleryFiles.filter(
file => file.isSelected === true,
);
try {
await utils.importFiles(selectedFiles[currentIndexWantToImport]);
setCurrentIndexWantToImport(currentIndexWantToImport++);
console.warn('Oh God!!!');
} catch (error) {
console.error(error);
}
})();
}, [currentIndexWantToImport]);
return (
<Contrainer>
<TopNavigation title='Importing files...' disableIconLeft />
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Text hint>0 / 20</Text>
</View>
</Contrainer>
);
};
const mapStateToProps = ({userGalleryFiles}) => ({userGalleryFiles});
export default connect(mapStateToProps)(FileImporter);
Now you have The pure of Recursive Functions
from React :)