Redux-Thunk vs. Redux-Saga

While building my first basic CRUD (create, read, update and delete) application, I was stumped as to how to integrate my axios data request into a Redux action creator. How do I bake in this layer of asynchronous code and feed the data to my Redux store? After researching, I found Redux-Thunk middleware to be a possible solution. This super helpful middleware allowed me to write an intermediary function, a thunk, that would make the ajax request and then call the action creator using the data received in the response of the object. As I continued to read other articles, I found that this was only one solution of several middleware options available.

To begin, what is a thunk?

A thunk is a function that acts as a wrapper in that it wraps an expression to delay its evaluation. For example, in the below code, the foo function acts as a thunk as it delays the calculation of the mathematical expression, 1 + 2.

According to Eric Raymond, the inventors of the thunk coined the term “after they realized (in the wee hours after hours of discussion) that the type of an argument in Algol-60 could be figured out in advance with a little compile-time thought […] In other words, it had ‘already been thought of’; thus it was christened a thunk, which is ‘the past tense of “think” at two in the morning”.

In the context of Redux, Redux-Thunk middleware allows you to write action creators that return a function instead of the typical action object. The thunk can then be used to delay the dispatch of an action until the fulfillment of an asynchronous line of code (e.g., an axios request to receive data).

Below is an outline of the step-by-step process of Redux-Thunk:

  1. Check what the incoming action is:

If it is a regular action object, Redux-Thunk does not do anything and the action object is processed by the store’s reducer

2. If the action is a function, Redux-Thunk invokes it and passes it the store’s dispatch and getState methods and any extra arguments (e.g., axios)

3. After the function runs, the thunk then dispatches the action, which will then update the state accordingly

Thus, in summary there are two parts to Redux-Thunk:

  1. A thunk creator, which is an action creator that returns a thunk (a.k.a. asynchronous action creators)
  2. The thunk itself, which is the function that is returned from the thunk creator and accepts dispatch and setState as arguments

The reason that we need to use a middleware such as Redux-Thunk is because the Redux store only supports synchronous data flow. Thus, middleware to the rescue! Middleware allows for asynchronous data flow, interprets anything that you dispatch and finally returns a plain object allowing the synchronous Redux data flow to resume. Redux middleware can thus solve for many critical asynchronous needs (e.g., axios requests).

Below is a graph of the npm downloads, over the past six months, for these other middleware options. The other most commonly used middleware is Redux-Saga.

Redux-Saga is a library that aims to make application side effects (e.g., asynchronous actions such as fetching data) easier to handle and more efficient to execute.

The idea is that a saga is similar to a separate thread in your application that’s solely responsible for side effects. However, unlike Redux-Thunk, which utilizes callback functions, a Redux-Saga thread can be started, paused and cancelled from the main application with normal Redux actions. Like Redux-Thunk, Redux-Saga has access to the full Redux application state and it can dispatch Redux actions as well.

To do this, Redux-Saga utilizes a new ES6 feature called generators. Generators are functions which can be exited and later re-entered.

Simply calling a generator function, marked by the asterisk to the right of the function keyword, will not cause the body of the function to execute immediately. Instead an iterator object is returned (e.g., gen in the above example). When the iterator’s next method is invoked, the generator function’s body is executed up until the first yield (e.g., line 2 above). The iterator’s next method returns an object with a value property containing the yielded value and a done boolean property, which indicates whether the generator has yielded its last value.

But let’s take a step back. What is yield?

Line 2 above is called a ‘yield expression’ because when you restart the generator, you will send back in the value and it will be used as the newly evaluated value. In essence, the yield keyword makes a request for a value.

Unlike typical JavaScript functions, generator functions run through to completion. Generator functions can be ‘cooperative’, which is a function that chooses when it will allow an interruption, so that it can cooperate with other code. You can use the yield keyword to pause the function from inside of itself. Note that nothing can pause the generator from the outside. However, once paused the generator cannot restart itself. Only an external control can be used to restart the generator function.

So who wins? Redux-Thunk or Redux-Saga?

Neither. Below is the same example with the first version leveraging Redux-Thunk and the second Redux-Saga.

Redux-Thunk version
Redux-Saga version

The benefit to Redux-Saga in comparison to Redux-Thunk is that you can avoid callback hell meaning that you can avoid passing in functions and calling them inside. Additionally, you can more easily test your asynchronous data flow. The call and put methods return JavaScript objects. Thus, you can simply test each value yielded by your saga function with an equality comparison. On the other hand, Redux-Thunk returns promises, which are more difficult to test. Testing thunks often requires complex mocking of the fetch api, axios requests, or other functions. With Redux-Saga, you do not need to mock functions wrapped with effects. This makes tests clean, readable and easier to write.

Redux-Thunk, however is great for small use cases and for beginners. The thunks’ logic is all contained inside of the function. Additionally, you do not need to learn a new function type, generators, and the keywords and methods associated with this function type. In conclusion, there are tradeoffs for each middleware and depending on your project, you can decide which middleware is most fitting for your code.