Working offline
6 September 2020
Recently I was working on a cross-plattform app built with React Native for Web. It's an e-learning tool with video courses, and one important requirement was that the app should be able to work offline. In this article I want to explain how we did that.
If you want to learn more about the project in general, check out my overview article.
Table of contents
Working offline
Developing the offline functionality in our app meant two things:
- We need to take care of requests made when the user is offline unintentionally. The app should handle situations without network connection gracefully and provide a reasonable user experience even when we can't handle their requests immediately.
- We need to provide a way to let the user explicitly download the data needed to complete a course offline.
Redux Offline and Redux Persist
If you've never heard about Redux, I suggest you have a look at their beginner's guide: Getting Started with Redux
There is a very handy package called Redux Offline, which makes it easy to send requests and define what should happen when a network error occurs. It comes with Redux Persist, a package which saves and rehydrates our Redux state in the user's local storage. If a network error happens during a request, the information about that request gets stored in a queue ("outbox") in the Redux state and persisted in the user's local storage. That way Redux Offline is able to retry those requests again, until they are successful.
You need to define an action for each request, with offline metadata containing the following three keys:
effect
: what request do you want to try?commit
: what do you want to do when the request was successful?rollback
: what do you want to do when the request failed permanently (i.e. not a network error but a "real" error happened)?
Here is an example:
const getCourse = (courseId) => {
return {
type: 'GET_COURSE',
meta: {
offline: {
effect: {
url: API_URL + '/api/courses/' + courseId,
method: 'GET'
},
commit: {
type: 'GET_COURSE_COMMIT',
},
rollback: {
type: 'GET_COURSE_ROLLBACK',
meta: {
then: (payload) => handleError(payload)
}
}
}
}
}
}
Note: The
then
property is not included in redux-offline. It comes from another middleware called redux-offline-chain that allows us to chain multiple actions, which was very helpful for example to do additional error handling after the rollback action was called.
The different caching strategies are very well explained in the basics section of the Redux Offline documentation, so I won't go into detail about that here, but to summarize there are basically two patterns you can use: the pessimistic and the optimistic update strategies.
- You can be optimistic about updates. The user should send whatever they like and be optimistic that it will eventually be saved on the server. If not, we can call the
rollback
action to reset the data to the previous state and notify the user that something went wrong. - When you depend on the response from the server for example to render something on the screen, you need to be pessimistic. You could show the most recent data you already have in your storage first and then try to load new data from the server and update the screen accordingly if it was successful. This is what the
commit
action is used for.
React Native File System
Now we are more or less able to use the app without a network connection. Once we have opened a course, the data is downloaded and persisted on our device. No errors or blank screens get us into trouble. But that was just the first step. Our courses certainly contain more than simple JSON data. First and foremost there are lots of videos. So we need to allow the user to download all the files necessary for a course.
To do this we add a download button to the course. If the user clicks it, we go through the course data and grab all the file urls we need to download. We create a folder for each course and put the files in there. For checking file stats, creating directories, downloading and deleting files, we can use a package called RNFS. This also lets us know how much progress we made, so we can show a little progress bar to the user.
Note: To download data onto the user's device, we need to access the file system. Therefore we need the user's permission before doing that.
We also have to take care of how many files we download at a time. We had some problems when downloading all at once, because we had lots and lots of large files, which caused the download to interrupt in between with an unexpected end of stream error. Our solution was to start multiple downloads at a time, but limit the number of concurrent downloads.
Summary
Making an app work offline requires a lot of consideration, because we have to think through all the possible cases. Redux Offline allows us to just react to the different cases and handles everything else for us. To get the full offline experience, we also need to make sure, the user has every file they need stored on their device. When doing everything right, the user will get a really nice user experience.
I have written more about React Native for Web: