ReactJS - Persisting State And Handling Offline With Redux

ReactJS - Persisting State And Handling Offline With Redux

Creating and supporting the offline mode varies depending on whether we are dealing with a mobile or a web app. In the case of a web app, it is rather hard to fetch a page content from the server so this type of app will not open at all without a stable connection. On the other hand, with a mobile application, we can store the entire code as well as the data (creating the app state based on this data).

We can distinguish two concepts here:

  • keeping the data in the application
  • optimistically apply actions to send requests to the server

For keeping the data stored in the application we can use redux-persist, which during a process called ‘rehydration’ restores the last known content of the store. The users can use and modify the store without the need to fetch fresh data, especially when the access to it is per-user only.

The second concept is optimistically applying actions, which send requests to our API. Since we are aware of how the application works and how the data should change we can replicate this logic and change the state of the application no matter if the request was fulfilled or not.

Here’s how it looks like:

const sendComment = ({ content }) => dispatch => {
 const tempId = _.uniqueId();
 dispatch({
   type: ADD_COMMENT_REQUEST,
   payload: { content, id: templId }
 });
 fetch(`mysuperawesomeAPI.pl/comments`, {
   method: "POST",
   headers: {
     Accept: "application/json",
     "Content-Type": "application/json"
   },
   body: JSON.stringify({
     content
   })
 })
   .then(payload =>
     dispatch({ type: ADD_COMMENT_SUCCESS, payload, tempId })
   )
   .catch(error =>
     dispatch({ type: ADD_COMMENT_FAILURE, tempId, error })
   );
};

and a reducer that will handle such action’s creator:

const comments = (state = [], action) => {
 switch (action.type) {
   case ADD_COMMENT_REQUEST:
     return [...state, payload];
   case ADD_COMMENT_SUCCESS:
     const tempIndex = _.findIndex(state, {
       id: action.tempId
     });
     return [
       _.slice(state, 0, tempIndex),
       action.payload,
       _.slice(state, tempIndex, state.length - 1)
     ];
   case ADD_COMMENT_FAILURE:
     const tempIndex = _.findIndex(state, {
       id: action.tempId
     });
     if (WE_WANT_TO_DELETE_ELEMENT) {
       return [
         _.slice(state, 0, tempIndex),
         _.slice(state, tempIndex, state.length - 1)
       ];
     }
     if (WE_WANT_TO_NOTIFY_USER_ABOUT_THIS_FACT) {
       return [
         _.slice(state, 0, tempIndex),
         { ...state[tempIndex], error: action.error },
         _.slice(state, tempIndex, state.length - 1)
       ];
     }
     return state;
   default:
     return state;
 }
};

So to optimistically apply actions means to change the state of the store based on the available data performed with a request sent in the background. A library helping to implement this way of managing actions is redux-optimist.

A library can restore the previous state of the store the minute the problems with requests occur. The actions should have an additional key ‘optimist’, in which we specify is it a BEGIN/COMMIT/REVERT of the transaction and a transactionID.

An efficiently working offline mode handling should also consider aspects like:

  • an attempt to resend the request when getting an incorrect reply from the server (for example, timeout)
  • sending requests only when we detect an internet connection or when our API is up
  • action queuing - not letting the requests be asynchronous
  • persisting a queue of actions between the relaunch of the app

Redux-offline is a complete solution, which implements the above functionalities and additionally allows you to configure:

  • the time period between requests send to the server when resending requests
  • the number of failed requests before rollbacking
  • the library for handling those requests (for example, axios)
  • queue implementations
  • changing the configuration of redux-persist

Problems will have to face:

  • usually the need to rewrite actions and reducers so they use redux-offline
  • sometimes implementation of business logic in the front-end (predicting how the server data will change)
  • rollback handling - we decide how the application will act when a rollback occurs
  • correct detection of an internet/API connection
  • the necessity to use standard objects in the action object - forget about callbacks

Using this implementation we can rewrite our action creator to look like this:

const addComment = content => dispatch => {
 const tempId = _.uniqueId();
 dispatch({
   type: ADD_COMMENT_REQUEST,
   payload: { content },
   meta: {
     offline: {
       // the network action to execute:
       effect: {
         url: "mysuperawesomeAPI.pl/comments",
         method: "POST",
         json: { content }
       },
       // action to dispatch when effect succeeds:
       commit: { type: ADD_COMMENT_SUCCESS, meta: { tempId } },
       // action to dispatch if network action fails permanently:
       rollback: { type: ADD_COMMENT_FAILURE, meta: { tempId } }
     }
   }
 });
};

Everything should work just fine with a basic configuration of redux-offline. The store implementation remains practically the same, we only need to change the way of pulling templd (now from the meta.templd key).

However, we need to remember that if the user is unable to open our page he will not be able to download the logic and therefore the website will not load. This is not the case in ReactNative where we have access to the application logic right after app open.