How to Build Offline-first Progressive Web Apps (PWAs) with React & Redux

November 17, 2020 Damian Kowalski

There are still many places around the world where Internet access is either poor or nonexistent. A plane or factory basement are prime examples – but, obviously, they’re just the tip of the iceberg. If you’re offering a mobile app, it’s a challenge to make it work under such conditions.

I had such a challenge in my last project and decided to share my experience. Now, it’s time to give you some hints on how to build web apps that work fully offline.

How Does It Works by Default

PWAs are web apps with some additional capabilities. By default, the offline mode will greet you with the infamous T-Rex (at least if you are using Chrome) just like on any other web page – but we have all the tools to change this!

PWA in offline mode without caching
PWA in offline mode without caching

Read Resilience

 

The goal is to offer the users access to the app files and data they previously saw without network access. This is a perfect job for a Service Worker — prefetching and caching data for later use. It’s not a big deal to tell SW to cache all resources you think are necessary to run your PWA. If you bootstrapped your project with CRA there’s already a predefined set of rules and a service worker generation script included.

But there’s one problem – they’re not very flexible. As a result, I ended up using my own configuration with Workbox. For more details on how to use the custom service worker in CRA click here. The default setup is good enough if you only care about static assets caching like JS, CSS files, fonts or images. I wanted something more — translations and data which, in my case, both came from our REST API.

Since /translations/{locale} endpoint was something I didn’t keep in the application state (managed by Redux) but simply passed it to i18next plugin, I thought it’s a good idea to tell the Service Worker to cache its response as well.

Below you can see my setup with translations caching included:

But what about the data coming from the API? I mentioned that the application state is managed by Redux. I won’t go too deep what Redux is – if you don’t know it very well, you probably at least heard about it. But one important thing you need to remember about Redux is that application state is kept in a single object called Redux store. And because it’s a single object, it’s also easy to save this object somewhere in the browser.

There is a package that does exactly that called redux-persist. By default, it saves the state in localStorage so you can still display everything you previously had in your Redux store in offline mode. You can also use a different storage mechanism ex. IndexedDB. It may be a better choice if you worry about the storage size.

Side Note: Redux-persist is a very popular package in React Native development to mimic a Native App’s behaviour on persisting the state between sessions in AsyncStorage

After all these steps, the user can now access your app in offline mode. Congrats!

But this was just the first step.

PWA in offline mode with SW caching and redux-persist
PWA in offline mode with SW caching and redux-persist

Write Resilience

 

As you can see on the screens, our app has a few “plus” icons. We could stop here and say “Sorry, adding or editing anything is not supported in offline mode” which is perfectly fine for some businesses. But what if this feature is highly demanded (or even crucial) for a given business?
First, let’s concentrate on how we normally talk to the API and react to its response:

Adding new contact  — the  UI is updated after the successful API response
Adding new contact  — the  UI is updated after the successful API response

This is fine for apps that require an internet connection to be available. Since obviously, we cannot rely on API in offline mode ,  maybe we should update UI right away and trigger API update in the background?

Optimistic UI

 

There is a UX pattern for such behaviour called Optimistic UI. It seems to be a good choice for our case.
In this approach we update the UI regardless of the API response, assuming that for the majority of requests it’s going to succeed. Here’s how adding a new contact will look like with this pattern:

Adding new contact  —  Optimistic UI approach
Adding new contact  —  Optimistic UI approach

How will this be different in offline mode? In terms of UI, not much  —  we just won’t show the linear progress since we cannot call the API offline. Still, we have to make sure this call is triggered after we’re back online. We could implement the logic ourselves, but instead of reinventing the wheel, let’s see what the redux-offline package has to offer.

redux-offlinePersistent Redux store for Reasonaboutable ™️ Offline-First applications, with first-class support for optimistic UI…www.npmjs.com

Looks like it has everything we need… and even more! Just to name a few key features:

  • Built-in redux-persist to save the state for offline with the ability to opt-out and use custom setup
  • Offline/Online detection mechanism
  • Managing requests queue
  • Retry mechanism in case of flaky networks

The only thing we have to make sure after setting up the library is to always use a specific format of redux actions for all of them that needs to send data to the backend. Here’s an example of how adding a new contact looks like with redux-offline:
In this case, the optimistic update will be done right after contact person/create action is dispatched. Other 2 actions will be dispatched only when the user is online.

Redux-offline actions dispatching logic
Redux-offline actions dispatching logic

The only piece of the puzzle I didn’t explain yet is why we generate IDs for new entities on the client-side. It’s because of relations.
Imagine we want to use our newly created contact in offline mode in another request. This could be assigned a contact to a visit note for example. Keep in mind we are still OFFLINE. In order to not duplicate the data, the payload in this new request should contain the ID of the contact we previously created.

1
2
3
4
5
6
PUT /visit-note/1
{
visitDate: '2020-10-23',
note: 'My visit',
contactPersonId: 'uuid-generated-on-client-side'
}

Obviously, since the previous POST request didn’t go to the API yet, we cannot rely on IDs generated on the server-side.
As soon as the connection is back, all the requests from the queue are triggered one after another in the order they were placed in the offline queue.
That said, we now have the possibility to modify the data in offline mode.

Conclusion

It’s not a trivial task to add offline support to your app. Fortunately, thanks to Service Workers and libraries like redux-persist and redux-offline, it’s not a huge development effort – you only need to understand the concepts to use them without confusion.

Hopefully, this article revealed some of the magic behind the offline-first architecture and will help you to include this functionality in your projects!

Last posts