Asynchronous & Offline Magic Tricks

in React Native

Woody Rousseau

Deputy CTO - Theodo UK

https://twitter.com/WoodyRousseau

https://github.com/wrousseau

Theodo

Helping our clients seize the opportunity of the digital transformation since 2009

 

Let's go back in time

The "Can you get the page to appear faster?" era

  • Creating database indexes
  • Caching everything
  • Compressing stuff

The "The carousel widget is broken again" era

  • Finding weird jQuery widgets on the internet
  • Fixing regressions due to untested JS code

The "The app doesn't work in the subway" era

  • Writing cool React/RN components
  • Doing fancy state management
  • Expecting the app to always behave as expected

Some made up Stats

for App Usages

12% - Dentist Waiting Room - Poor 3G

Some made up Stats

for App Usages

12% - Dentist Waiting Room - Poor 3G

27% - Your Office's toilets - Great Wifi

Some made up Stats

for App Usages

12% - Dentist Waiting Room - Poor 3G

27% - Your Office's toilets - Great Wifi

29% - Awful Tinder date at restaurant - Good 4G

Some made up Stats

for App Usages

12% - Dentist Waiting Room - Poor 3G

27% - Your Office's toilets - Great Wifi

29% - Awful Tinder date at restaurant - Good 4G

78% - The Subway - ?

An App:

  • Must handle failure
  • Should avoid failure

Must handle failure

Implementing each feature for 3 states

  • Expected State
  • Loading State
  • Blank State

Defensive Design

Should avoid failure

Decreasing the probability for a user to encounter loading and blank states

1. Reading
2. Writing

Reading

Strategies
 

Start Defensive

  • Start with an ActivityIndicator
  • Facebook strategy: showing final layout with no content
  • For long requests: show progress and ease waiting (Postman strategy)

Loading state

Start Defensive

  • Fill the space!
  • Saving the day: Provide a Call to Action to get back to the normal state
  • Hijacking: Distract the user from their original desire

Blank State

Keep persisting

Store level

Server

Data

Redux Store

  • Foreground persistance 
  • App resuming persistance
  • Default with Redux

Keep persisting

Store level

// request the boards
export const boardsRequest = (state: Object) => state.merge({ error: false, fetching: true })
}

// successful boards lookup
export const boardsSuccess = (state: Object, action: Object) => {
  return state.merge({ fetching: false, error: false, boards: action.boards });
};

// failed to get the boards
export const boardsFailure = (state: Object) => state.merge({ error: true, fetching: false })

Keep persisting

Redux persist level

Server

Data

Redux Store

  • Restart persistance 
  • redux-persist

AsyncStorage

Persist

Rehydrate

Keep persisting

Size issues

  • redux-persist-transform-compress for compressing
  • redux-persist-transform-filter for filtering

AsyncStorage

Limit: Your device space on iOS, 6MB by default on Android

Keep persisting

Handling updates

const datingApps = [
    { id: 1, name: 'Tinder' },
    { id: 2, name: 'Bumble' },
];
const datingApps = {
    data: [
        { id: 1, name: 'Tinder' },
        { id: 2, name: 'Bumble' },
    ],
    fetching: false
};
const manifest = {
 1: (state) => state
 2: (state) => ({ ...state, datingApps: { data: state.datingApps, fetching: false }}),
};

Current manifest version

redux-persist-migrate

AsyncStorage

Keep persisting

Warning the user

import { NetInfo } from 'react-native';

function handleConnectivityChange(isConnected) {
  // Dispatch action to change the status in the state
}
NetInfo.isConnected.addEventListener(
  'change',
  handleFirstConnectivityChange
);

Finish by optimizing

Writing

Strategies
 

Start Defensive

Warning the user

react-native-dropdown-alert

Take risks

Be Optimistic

  • Change your UI when the request is attempted rather than waiting for the server response
  • Trigger a commit action when the server responds with a success
  • Trigger a rollback when the server responds with a failure

redux-optimist

Take risks

Be Optimistic

BLACK_REQUEST

BLUE_REQUEST

BLACK_ROLLBACK

BLUE_COMMIT

Redux Store

1

2

3

4

5

Take risks

Be Optimistic

Jani Eväkallio

  • Handles your read issues by wrapping redux-persist
  • Handles your write issues by implementing optimistic behavior and the queuing mecanism

 

const followUser = userId => ({
  type: 'FOLLOW_USER_REQUEST',
  payload: { userId },
  meta: {
    offline: {
      // the network action to execute:
      effect: { url: '/api/follow', method: 'POST', body: { userId } },
      // action to dispatch when effect succeeds:
      commit: { type: 'FOLLOW_USER_COMMIT', meta: { userId } },
      // action to dispatch if network action fails permanently:
      rollback: { type: 'FOLLOW_USER_ROLLBACK', meta: { userId } }
    }
  }
});

const followingUsersReducer = (state, action) {
  switch(action.type) {
    case 'FOLLOW_USER':
      return { ...state, [action.payload.userId]: true };
    case 'FOLLOW_USER_ROLLBACK':
      return omit(state, [action.payload.userId]);
    default:
      return state;
  }
}

Take risks

Be Optimistic

  • Change how network requests are made
  • Change the detection of network
  • Change the detection of irreconciable errors
  • Change the retry strategy

Take risks

... but warn your users!

Strategy:

  • Make the warning appear when requesting
  • Make the warning disappear on its own or if connexion is restored
  • Prevent next warning before a delay
export function * handleNetworkRequiringAction (action) {
  const onlineStatus = yield select(state => state.offline.online);
  if (!onlineStatus) {
    yield put({ type: 'SHOW_CONNEXION_ALERT' });
    const { connexionRestored, timeout } = yield race({
      connexionRestored: take(action => {
          return action.type === 'Offline/STATUS_CHANGED' && action.payload.online;
      }),
      timeout: call(delay, 4000),
    });
    yield put({ type: 'HIDE_CONNEXION_ALERT' });
  }
}

export function * watchNetworkRequiringActions () {
  while (true) {
    const action = yield take([
      BoardsTypes.SUBSCRIBE_TO_BOARD_REQUEST,
      BoardsTypes.UNSUBSCRIBE_TO_BOARD_REQUEST,
    ]);
    yield call(handleNetworkRequiringAction, action);
    yield call(delay, 6000);
  }
}
@connect(state => ({
  connexionAlert: state.connexionAlert,
}), mapDispatchToProps)
export default class BoardsScreen extends React.Component {
  componentWillReceiveProps(nextProps) {
    if (this.dropdown) {
      if (!this.props.connexionAlert && nextProps.connexionAlert) {
        this.dropdown.alertWithType('info', 'Info', 'Offline device');
      } else if (this.props.connexionAlert && !nextProps.connexionAlert) {
        this.dropdown.dismiss();
      }
    }
  }
  render() {
    return (
      <View>
        ...
        <DropdownAlert
          ref={(ref) => this.dropdown = ref}
        />
      </View>
    );
  }
};

Take risks

... but warn your users!

react-redux-saga-offline in preparation ;)

https://twitter.com/WoodyRousseau

Take risks

... and prepare for the worst

What if there is a conflict for PUT and PATCH requests?

BACK-END DEVELOPER ???

Take risks

... and prepare for the worst

id text createdAt updatedAt updateAttemptedAt
1 John 2017-01-01 2017-01-04 2017-01-03
2 Philip 2017-02-01 2017-02-04 2017-02-04

DB

F1

F2

F3

UA3

UA2

UA1

U1

U3

U2

Take risks

... and prepare for the worst

https://github.com/theodo-UK/offline-goodies

4 strategies
The idea: reject with a 409 if the update is rejected

Take risks

... and prepare for the worst

const rejectLaterAttemptedUpdate = (options = {}) => (hook) => {
  const updateAttemptDateLabel = options.updateAttemptDateLabel || 'updateAttemptedAt';
  return hook.app.service(hook.path).get(hook.id)
  .then(serverObject => {
    if (serverObject[updateAttemptDateLabel] < new Date(hook.data[updateAttemptDateLabel])) {
      const error = new errors.Conflict('There was a earlier modify attempt');
      return Promise.reject(error);
    }
    return Promise.resolve(hook);
  });
};

Take Home

  • Always start Defensive!
  • Offline specs are very feature dependant, don't generalize

Thank You

Woody Rousseau - woodyr@theodo.co.uk

Offline magic tricks

By Woody Rousseau

Offline magic tricks

  • 1,882