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?
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.
The ++code>fetch++/code> method returns what they call a ++code>StatefulPromise++/code> containing the following:
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>
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!