A Thunk, Saga and Epic walk into a bar...
Redux is a very popular tool for handle state management in a React application. However, it does not have a built-in way for handling side effects like API calls. There are multiple Redux middleware libraries that can help achieve this task like redux-thunk, redux-saga and redux-observable. I will share a basic example of using each library and how to integrate them in a React project using redux-toolkit (the package is intended to be the standard way to write Redux logic), but I would not go into advanced concepts like retries, throttling and so on.
A thunk, saga and epic walk into a bar, the bartender asks: “What is the state of affairs?” - Artur Daschevici
Redux Thunk
Redux-thunk is the official version of async function middleware suggested by Redux. The thunk middleware allows you write action creators that return a function instead of an action object. The functions receive dispatch and getState as arguments and can have async logic inside, and that logic can dispatch actions as needed. Writing thunk functions requires that the redux-thunk middleware be added to the store during the setup process. Redux Toolkit's configureStore function does this automatically.
When fetchUsersList is dispatched in a component or another thunk, the function it returns will receive the dispatch method. This can be used to toggle the loading state while we make the API call (which is a promise). If the promise resolves, we dispatch the getUsersListSuccess action. If it rejects and throws an error, we dispatch the getUsersListFailed action.
This pattern is easy to pick up, but does not scale easily as advanced concepts like cancellation and debouncing are not easy to implement.
Redux Saga
Redux-sagas use an ES6 feature called generator functions. It is defined by the function keyword followed by an asterisk (function*
). When a saga is called, it does not immediately execute the code within it, instead it returns a special type of iterator. The redux-saga middleware exposes some functions in its API like call, put and fork, which can be used to yield plain action objects.
The takeLatest helper function will watch for an action pattern to match and call a generator function when it does. If fetchUsersListSaga is already running it will be cancelled and a new instance spawned. Redux-Saga handles the promise returned by the API and either resolves a response or rejects with an error. The put effect instructs the middleware to perform a dispatch action.
Redux-Saga has a lot of built in function to handle advanced concepts we have mentioned before and is also very easy to test.
Redux Observable
Redux-Observable makes use of - you guessed it - observables. Observables are similar to functions but can return multiple values over time either synchronously or asynchronously. Functions cannot have multiple return values. Redux-Observable is a wrapper around RxJS with additional operations.
A redux observable epic function filters for one or more type of actions and can return multiple action streams. The observable stream of actions passed to an epic are referenced with a variable that commonly ends with $. Redux-observable is quite similar to redux-saga but the syntax is more declarative.
The match method of an action creator object returned by the createAction API of redux-toolkit can be passed to the filter operator from RxJS. We can then use the from helper function to create an observable from the promise returned by our API helper function. This new stream is piped to the map operator (you can use the mergeMap operator if you have multiple return values) if it resolved or the catchError operator if it rejects. We use the switchMap operator to do something similar to takeLatest in sagas - when a new inner Observable is emitted, switchMap stops emitting items from the earlier-emitted inner Observable and begins emitting items from the new one.
Redux-Observable is very powerful especially if you are familiar with RxJS, but the learning curve is steep and testing isn’t easy to learn.
In conclusion, if you need a middleware for a complex application, redux-saga or redux-observable are good choices. I’ve made use of both in production, but for small projects I’ll probably use redux-thunk. I hope this helps.