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

  • 590