Managing async
business logic
in flux-like apps
Maciej Stasiełuk, 29.09.20
Agenda
- Overview of different approaches
used in well-known and popular solutions - New cool kids on the block
- Open discussion
Flux approach
Action
Dispacher
View
Stores
Manages data flow in a unidirectional flow
Complicated, hard to maintain store models

Pure Redux approach
Action
Reducers
View
Store
dispatch({ type: SOME_ACTION, payload: id });
function reducer(state = {}, action) {
switch (action.type) {
case SOME_ACTION:
// sync business logic here
const updatedState = {};
return {...state, ...updatedState};
default:
return state;
}
}
Better, simpler architecture
Business logic must be synchronous

Fat action creators
Action
Reducers
View
Store
const doSomething = (dispatch, id) => {
dispatch({ type: SOME_ACTION, payload: id });
fetch(/**/).then(data => {
dispatch({ type: SOME_ACTION_SUCCESS, payload: data });
}).catch(err => {
dispatch({ type: SOME_ACTION_FAILED, payload: err });
});
};
Can perform async logic
No easy access
to the store
Redux Thunk
Action
Reducers
View
Store
const doSomething = id => (dispatch, getState) => {
const state = getState();
dispatch({ type: SOME_ACTION, payload: id });
fetch().then(data => {
dispatch({ type: SOME_ACTION_SUCCESS, payload: data });
}).catch(err => {
dispatch({ type: SOME_ACTION_FAILED, payload: err });
});
};
Simple and have
access to state
Cannot cancel or limit actions, hard to manage complicated logic
Middleware
Redux Saga
Action
Reducers
View
Store
const doSomething = id => ({ type: SOME_ACTION, payload: id });
function* watchDoSomething() {
while ( yield take(SOME_ACTION) ) {
const doSomethingTask = yield(fork(doSomething));
yield take (SOME_ACTION_CANCEL);
yield cancel(doSomethingTask);
}
}
function* doSomething(action) {
try {
const data = yield fetch(/**/);
yield put({ type: SOME_ACTION_SUCCESS, payload: data });
}
catch(err) {
yield put({ type: SOME_ACTION_FAILED, payload: err });
}
finally {
if (yield cancelled()) {
yield put({ type: SOME_ACTION_CANCELLED });
}
}
}
Support cancellation and limiting, so can manage complicated cases
Lots of boilerplate and generator syntax is hard to read for most people
Sagas

Redux Observable
Action
Reducers
View
Store
const doSomething = id => ({ type: SOME_ACTION, payload: id });
const soSomethingEpic = action$ =>
action$
.ofType(SOME_ACTION)
.mergeMap(action =>
ajax.get(/**/)
.map(data => ({ type: SOME_ACTION_SUCCESS, payload: data }))
.catch(err => ({ type: SOME_ACTION_FAILED, payload: err }))
.takeUntil(action$.ofType(SOME_ACTION_CANCELLED))
);
Can use full power of Observables and RxJS
Still some boilerplate and require RxJS knowledge
Epics

Redux Logic
Action
Reducers
View
Store
const doSomething = id => ({ type: SOME_ACTION, payload: id });
const doSomethingLogic = createLogic({
type: SOME_ACTION,
cancelType: SOME_ACTION_CANCEL,
process({ getState, action }, dispatch, done) {
fetch(/**/)
.then(data => dispatch({ type: SOME_ACTION_SUCCESS, payload: data }))
.catch(data => dispatch({ type: SOME_ACTION_FAILED, payload: data }))
.then(() => done());
}
});
Declarative API, supports everything mentioned before, easy supporf for complex cases, uses RxJS underneath.
Not very popular,
new API to learn
Logic
const doSomething = id => ({ type: SOME_ACTION, payload: id });
const doSomethingLogic = createLogic({
type: SOME_ACTION,
cancelType: SOME_ACTION_CANCEL,
processOptions: {
successType: SOME_ACTION_SUCCESS,
failType: SOME_ACTION_FAILED,
},
async process({ getState, action }) {
return await fetch(/**/);
}
});
Recoil
View
Atoms
const somethingQuery = selectorFamily({
key: 'SomeData',
get: id => async () => {
const data = await fetch(/**/);
return data;
},
});
// in view
function ShowSomethingComponent({ id }) {
const data = useRecoilValue(somethingQuery(id));
return <div>{data}</div>;
}
Much simpler, high performance, very React-focused (concurrent mode etc.)
Is not production ready yet.
Not sure if it will handle complex business logic
Selectors
Zustand
View
Stores
const useStore = create((set, get) => ({
someValue: {},
doSomething: async id => {
const data = await fetch(/**/);
set({ someValue: data });
}
});
// in view
function ShowSomethingComponent({ id }) {
const data = useStore(state => state.someValue);
return <div>{data}</div>;
}
Very simple but powerful.
Compatible with flux/redux architecture (devtools etc.)
Not very popular.
Not sure if it will handle complex business logic
Selectors
Action

Lots of other alternatives...
- MobX
- Apollo local state
- XState
- ...
Debate time
Questions?
flame war
Further reading
- https://github.com/jeffbski/redux-logic/blob/master/docs/where-business-logic.md
- https://facebook.github.io/flux/docs/in-depth-overview
- https://redux.js.org/understanding/thinking-in-redux/motivation
- https://github.com/reduxjs/redux-thunk#motivation
- https://redux-saga.js.org/docs/introduction/SagaBackground.html
- https://redux-observable.js.org/docs/basics/Epics.html
- https://recoiljs.org/docs/introduction/motivation
- https://github.com/pmndrs/zustand
Managing async business logic in flux-like apps
By Maciej
Managing async business logic in flux-like apps
- 733