Middleware for the Async Flow in Redux

Should we use Thunk, Promises, or Sagas?

Viduni Wickramarachchi
Bits and Pieces

--

Proper state management is vital for any React application. And still, Redux is dominating this space.

However, if you use Redux alone, you will feel a gap when using it for asynchronous operations.

And that’s where the Redux middleware comes into play. In this article, I will explore these middleware options in detail to understand their capabilities.

Async Operations in Redux

As you all know, state changes in the Redux store are performed by triggering the actions provided by the store.

The middleware that we use will intercept the action. It can also delay the actions if necessary, supporting asynchronous operations. And, once the asynchronous process completes, the rest of the Redux flow continues as usual, where the reducer receives the action to compute the new state.

Therefore, as mentioned above,

Async operations in Redux are achieved by making action creators asynchronous with the use of a middleware.

Role of Middleware in Redux

Applying any middleware to Redux is done by enhancing the createStore() function with the applyMiddleware() function by Redux. The middleware should be passed onto this function. Usually, the Redux store’s dispatch() function only allows dispatching an object, which is called an action.

However, when a middleware is applied, it wraps the store’s dispatch() function, enabling the dispatch of functions or promises instead of actions.

The middleware intercepts the dispatch, allowing the asynchronous data flow, and finally dispatching the action. The final dispatch of the action restores the synchronous data flow.

There are many techniques that can be used as middleware for Redux.

  • Thunk middleware.
  • Saga middleware.
  • Promise-based middleware.
  • Async/Await (with the help of Thunk middleware).

Let’s look at each of their capabilities.

Thunk Middleware

Redux thunk is a very easy way of introducing a middleware to Redux. It’s even mentioned as the solution for async operations in Redux in the official redux documentation.

Redux Thunk does the following.

  • Allows writing action creators that return a function instead of an action.
  • Allows delaying the dispatch of an action.
  • Allows dispatching an action if a certain condition is met.
  • Passes dispatch() and getState() as parameters to the function that’s returned by the action creator (the inner function).

Once Redux Thunk is introduced, the action creator is called a Thunk.

Pros

  • Suitable for simple applications.
  • Enables async operations without a lot of boilerplate code.
  • Easy to set up and implement — Less learning curve.

Cons

  • Cannot act in response to an action.
  • Difficult to handle concurrency problems that may occur.
  • Imperative — Not very easy to test.
  • Does not scale well — As the application grows you might end up with complicated unmanageable code.

Saga Middleware

Redux Saga uses an ES6 feature called Generators to enable async operations.

Generators make it easy to read, write and test asynchronous flows.

The saga middleware exposes a set of helper functions to create declarative effects (plain JavaScript objects) that can be yielded by our sagas. The middleware will then handle the objects yielded behind the scenes.

Pros

  • Allows expressing complex logic as pure functions (Sagas).
  • Easy to test because pure functions are predictable, repeatable and the effects are declarative.
  • Can act in response to actions — A saga subscribes to the store and allows triggering sagas when an action is dispatched to continue or run.
  • Able to use takeLatest which will allow using the last trigger (or the opposite behavior with the use of takeEvery)— Helps avoid concurrency problems.
  • Decouples effects — For example, when we click on Button 1 if Button 2 should change, Button 1 only dispatches that it was clicked. Then the saga listening for this button click and will update Button 2 by dispatching a new event that Button 2 is aware of.
  • Allows separation of concerns.
  • Only events are fired (not actions) — This makes it easier to handle the UI as a translation layer between what has happened and what should happen as a result of the effect, does not have to be maintained.
  • Sagas can be time-traveled and enables complex flow logging.
  • Has built-in ways to handle advanced tasks like throttling, debouncing, race conditions, and cancellation.
  • Makes it easier to scale complex applications with side effects.
  • Easier to catch errors and handle failures (via try catch blocks).
  • Well documented.

Cons

  • Not very suitable for simple apps as it adds unnecessary indirection of triggers.
  • More boilerplate code than other middlewares.
  • Need to have the knowledge of generators to fully grasp the concepts.
  • Higher learning curve than other middlewares.

Apart from these two middlewares (which I would say are the most popular for handling async operations in Redux), there are other middlewares that we can explore as well, such as the promise middleware.

Promise-based Middleware

The promise middleware returns a promise to the caller in order to wait for the async operation to be completed. This is useful for server-side rendering. There are few packages that can be used as promise based middleware (E.g.- redux-promise-middleware ).

Pros

  • Easy to understand — Less learning curve.
  • Ability to use promises.
  • Enables optimistic updates.
  • Less boilerplate code than Sagas.

Cons

  • Only works for trivial cases — Does not scale well.
  • Cannot chain operations together.
  • No way to cancel a promise — unlike Sagas.
  • Does not dispatch plain action objects — Harder to test.

A promise middleware can be combined with Thunk if chaining async operations are required.

Async/Await is also built on top of promises where async code looks more like synchronous code. We do not require any special middleware to use async/await for async operations.

However, similar to other promise-based middleware, this too requires the help of Thunk to chain async operations.

Build applications differently

OSS Tools like Bit offer a new paradigm for building modern apps.

Instead of developing monolithic projects, you first build independent components. Then, you compose your components together to build as many applications as you like. This isn’t just a faster way to build, it’s also much more scalable and helps to standardize development.

It’s fun, give it a try →

An independently source-controlled and shared “card” component (on the right, its dependency graph, auto-generated by Bit)

Conclusion

All of the middleware we discussed in this article are extremely popular and fits different use cases. If you are a beginner at Redux, I suggest trying out Redux Thunk first, as it is straightforward to understand and implement. Once you have the hang on Thunks, moving onto promise-based middleware and Sagas makes more sense.

For large applications that need scaling, Redux-Saga will be the best option as it handles application side effects well, easy to test, and makes it easier to handle errors. In addition, Redux-Saga is feature-rich which will be necessary for enterprise-grade applications.

Further, there are newer solutions such as Redux Observables for handling async operations. This is built around observables and the reactive pattern. If you are interested in this pattern, more information can be found here.

Let me know your thoughts on the middleware discussed in this article. Thanks for reading!

--

--