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
- 2,076