1

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

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Firmansyah
  • 106
  • 12
  • 27
  • 1
    Start by [never passing an `async function` as the executor to `new Promise`](https://stackoverflow.com/q/43036229/1048572)! – Bergi Dec 14 '20 at 22:00
  • Just check that `has_canceled` variable before each asynchronous (`await`ed) step. – Bergi Dec 14 '20 at 22:02
  • @Bergi Thanks for the comment but, I still didn't understand what you saying *check `hasCanceled_` variable* – Firmansyah Dec 15 '20 at 01:53

2 Answers2

1

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

Angular San
  • 356
  • 1
  • 8
0

With the custom promise (c-promise) you can do the following (See the live demo):

import { CPromise, CanceledError } from "c-promise2";

const delay = (ms, v) => new Promise((resolve) => setTimeout(resolve, ms, v));

const importFile = async (file) => {
  return delay(1000, file); // simulate reading task
};

function importFiles(files) {
  return CPromise.from(function* () {
    for (let i = 0; i < files.length; i++) {
      try {
        yield importFile(files[i]);
      } catch (err) {// optionally
        CanceledError.rethrow(err);
        console.log(`internal error`, err);
        // handle file reading errors here if you need
        // for example if you want to skip the unreadable file
        // otherwise don't use try-catch block here
      }
    }
  }).innerWeight(files.length);
}

const promise = importFiles([
  "file1.txt",
  "file2.txt",
  "file3.txt",
  "file4.txt"
])
  .progress((value) => {
    console.log(`Progress [${(value * 100).toFixed(1)}%]`);
    // update your progress bar value
  })
  .then(
    (files) => console.log(`Files: `, files),
    (err) => console.warn(`Fail: ${err}`)
  );

setTimeout(() => promise.cancel(), 3500); // cancel the import sequence
Dmitriy Mozgovoy
  • 1,419
  • 2
  • 8
  • 7