Building apps that know how to fail with grace
in the unpredictable world of the asynchronous web
before we start, a quick intro to ES6
let status = 'nothing';
function* simpleGen(x) {
status = 'started';
yield x * 2;
yield x * 4;
yield x * 6;
status = 'done';
}
const it = simpleGen(1);
it.next();
function* twiceAndQuadruple(x) {
yield x * 2;
yield x * 4;
}
function* twiceAndQuadrupleForever(x) {
const whatever = x;
while(true) {
yield* twiceAndQuadruple(x);
x += 1;
}
}
const x = twiceAndQuadrupleForever(1);
x.next();
Generators Deep Dive
function* speakToMe(name) {
const y = yield `Hey! ${name}? How are you?`;
const res = yield `Did you say ${y}?`;
if (res === 'yes') {
yield 'Aight nice to meet you.';
} else if (res) {
yield 'My bad!';
}
yield 'Aight, suit yourself bye.'
}
const x = speakToMe('Martin');
x.next();
Generators Deep Dive
function request({ url }) {
fetch(url).then((data) => {
it.next(data);
});
}
function* main() {
const res1 = yield request({ url: 'someUrl' });
const { data: { id } } = JSON.parse(res1);
const res2 = yield request({ url: `someUrl/${id}` });
const output = JSON.parse(res2).data;
console.log(output);
yield output;
}
const it = main();
it.next();
Async Side Effect Management
enter sagas
redux-saga
benefits
Declarative effects.
Advanced async control flow
concurrency management.
Architectural benefits...SoC+colocation= maintainability
more insight I guess
effects at a glance
function* dragWatcher({ payload }) {
while(true) {
yield take('MOUSE_DOWN');
const nextAction = yield take('MOUSE_MOVED', 'MOUSE_UP');
if (nextAction.type === 'MOUSE_MOVED') {
yield put({ type: 'USER_STARTED_DRAGGING' });
yield take('MOUSE_UP');
yield put({ type: 'USER_STOPPED_DRAGGING' });
}
}
}
trivial example?
thunks
function login({ user, pass }) {
return async (dispatch, getState) => {
// await dispatch(api(....)) why!!? :(((
try {
await api({
method: 'GET',
path: '/v2/users/login',
});
dispatch({ type: 'LOGIN_SUCCESS' });
dispatch(fetchSuperPrivateInfo());
} catch (e) {
dispatch({ type: 'LOGIN_FAILED' });
}
};
}
function logout() {
return async (dispatch, getState) => {
try {
await cleanup();
dispatch({ type: 'LOGOUT_SUCCESS' });
} catch (e) {
dispatch({ type: 'LOGIN_FAILED' });
}
};
}
function fetchSuperPrivateInfo() { ... }
saga
function* authSaga() {
try {
const token = yield call(api, {
method: 'GET',
path: '/v2/users/login',
});
yield put({ type: 'LOGIN_SUCCESS', payload: { token } });
const task = yield fork(getSuperPrivateInfo); // composable!!!
yield take('LOGOUT_REQUEST');
yield cancel(task);
// getSuperPrivateInfo can abort fetch/do cleanup, whatever!
try {
yield call(cleanup);
yield put({ type: 'LOGOUT_SUCCESS' });
} catch (e) {
yield put({ type: 'LOGIN_FAILED' });
} // yield* logoutSaga();
} catch (e) {
yield put({ type: 'LOGIN_FAILED' });
}
}
function* getSuperPrivateInfo() {}
function* cartRemovalConsequenceManager(action) {
const { payload: { product } } = action;
const { type, product } = yield take([
'REMOVE_FROM_CART_SUCCESS',
'REMOVE_FROM_CART_ERROR',
]);
if (type === REMOVE_FROM_CART_ERROR) {
yield put(addToLocalCart(product));
yield put(addNotification('Please try again.'));
}
if (type === 'REMOVE_FROM_CART_SUCCESS') {
yield put(saveToRecentRemovals(product));
const { undo } = yield race({
undo: take(action => (
action.type === UNDO_CART_REMOVAL
&& action.payload === product.id
)),
timeup: delay(7500),
});
yield put(removeFromRecentRemovals(product.id));
if (undo) {
yield put(addToCart(product));
}
}
}
// state = {
// recentRemovals: [],
// cartItems: [],
//}
simple undo
function* likeSaga(id) {
try {
const response = yield call(api.likePost, id);
yield put(likePostSuccess(response));
} catch (e) {
yield put(likePostError(e.message));
} finally {
yield call(api.unlikePost, id);
}
}
function* likeWatcher({ payload }) {
const { id } = payload;
yield put(likeOptimistically(payload));
// shameless Ramda plug
const { userUndid, timeup } = yield race({
userUndid: take(allPass([
propEq('type', UNLIKE_POST_REQUEST),
pathEq(['payload', 'id'] === id),
])),
timeup: delay(5000),
});
if (userUndid) {
yield put(undoOptimisticLike(id));
} else if (timeup) {
const task = yield fork(likeSaga, id);
const { type } = yield take([
axn => axn.type === UNLIKE_POST_REQUEST && axn.payload.id === id,
axn => axn.type === LIKE_POST_SUCCESS && axn.payload.id === id,
axn => axn.type === LIKE_POST_ERROR && axn.payload.id === id,
]);
if (type === UNLIKE_POST_REQUEST) {
yield cancel(task);
}
}
}
where sagas truly shine for me
Infinite loops that work as daemons, but are slaves to a parent saga that might kill them...refresh authentication tokens reliably, or perform some similar "background" task intelligently.
Retrying tasks...with granular control over cancel/retry strategies.
benefits in retrospect
A pure system with pure actions that can be OBSERVED to perfect granularity without relying on "Network" tab or searching tens of components to figure out what the heck is going on. Real time travel.
Ability to change app logic at one place, even though the affected components are many!
Easy testing. Tests that are "quick" and don't take more time than writing the actual functionality (I'm looking at you, async thunks!)