React Native

Handling Download Progress with Redux Saga

You probably know how to use methods returning promises in a Redux-Saga but have you ever wondered what to do in case you need to track progress?

Setting up Redux + Redux-Saga in a React Native Project

Recently I have started a React Native project in which I setup my now usual stack for managing app state, Redux + Redux-Saga. Everything was going well until I installed the React Native Fetch Blob in order to be able to download files in my app.

StatefulPromise Returned by the fetch Method

The ++code>fetch++/code> method returns what they call a ++code>StatefulPromise++/code> containing the following:

  • ++code>cancel():Promise++/code>
  • ++code>progress(config, (received, total) => void)++/code>
  • ++code>then((result) => void)++/code>
  • ++code>catch((error) => void)++/code>

Handling Progress and Cancellation in Redux-Saga

If you are used to the ++code>call++/code> method, you probably know by now that it can only handle a regular promise. But here we also want to handle progress and to be able to cancel the download.

This is where Saga's ++code>eventChannel++/code> comes into play. As you will see, eventChannel will allow you to setup regular functions with multiple callbacks to be used in sagas. The first step is to take the ++code>fetch++/code> method and wrap it in a Saga helper.

++pre>++code class="language-javascript">import { eventChannel, END } from 'redux-saga';
import RNFetch from 'react-native-fetch-blob';

export function downloadFileSagaHelper(_id: string, url: string, filePath: string) {
 // An event channel will let you send an infinite number of events
 // It provides you with an emitter to send these events
 // These events can then be picked up by a saga through a "take" method
 return eventChannel(emitter => {
   const task = RNFetch.config({
     path: filePath,
     addAndroidDownloads: {
       useDownloadManager: true,
       title: 'My download',
       notification: true,
     },
   })
     .fetch('GET', url)
     .progress({ interval: 200 }, (received, total) => {
       const progress = received / total * 100;
       // I chose to emit actions immediately    
       emitter({
         type: actionTypes.DOWNLOAD_FILE_PROGRESS,
         payload: { _id, progress },
       });
     })
     .then(res => {
       emitter({
         type: actionTypes.DOWNLOAD_FILE_FINISHED,
         payload: { _id, filePath: res.path() },
       });
       // END event has to be sent to signal that we are done with this channel
       emitter(END);
     })
     .catch(error => {
       // Optional but you can deal with the error here
       throw error;
     });

   // The returned method can be called to cancel the channel
   return () => {
     task.cancel();
   };
 });
}
++/code>++/pre>

You now have a method usable within a saga:

++pre>++code class="language-javascript">export function* downloadFileSaga(action) {
 const { _id, url, filePath } = action.payload;
 const channel = yield call(downloadFileSagaHelper, _id, url, filePath);

 try {
   // take(END) will cause the saga to terminate by jumping to the finally block
   while (true) {
     // Remember, our helper only emits actions
   // Thus we can directly "put" them
     const action = yield take(channel);
     yield put(action);
   }
 } catch (error) {
   put({
     type: actionTypes.DOWNLOAD_FILE_ERROR,
     payload: { _id, error },
   });
 } finally {
   // Optional
 }
}
++/code>++/pre>

Throttling Progress Events

A small drawback is that if the progress event is fired too often it will trigger too many dispatches and renders which in turn will freeze access to your store (and subsenquently your app). This is why we used the ++code>{ interval: 200 }++/code> progress option to limit calls.

If the library you use doesn't offer this option you can still use a throttler such as lodash's directly on the callback. Our progress method then ressembles this:

++pre>++code class="language-javascript">import throttle from 'lodash.throttle';
// ...
progress: throttle(
 (received, total) => {
   const progress = received / total;
   emitter({
     type: actionTypes.DOWNLOAD_FILE_PROGRESS,
     payload: { _id, progress },
   });
 },
 200
),
++/code>++/pre>

Hope that helps you setup file progress in your own project!

Développeur mobile ?

Rejoins nos équipes