Recently I joined an internship, the project I was assigned was written with redux-saga however, I had only known redux-thunk so I read some documentation and watched some videos on the saga, this article is to explain redux-saga in my own and easy way.
What is Redux-Saga and why to use it if we have Redux Thunk?
Redux Saga is a redux middleware that allows you to manage your side effects (for eg. reaching out to the network) just like Redux Thunk.
Redux Saga offers us a collection of helper functions that are used to spawn your tasks when some specific actions are dispatched. These can be used to help organize when and how your tasks are executed.
Know Generator Function before knowing Redux saga
Redux Saga uses generator functions a lot. Generator functions allow us to pause our functions and wait for a process to finish, which is similar to the thought process behind resolving promises.
function* myGenerator() {
const result = yield myAsyncFunc1()
yield myAsyncFunc2(result)
}
We will look at a high-level overview of how we would plan out these sagas within our app’s architecture.
The Root Saga
Similar to how reducers in Redux are organized in that we have a root reducer which combines other reducers, sagas are organized starting by the root saga.
function* rootSaga() {
yield all([
menuSaga(),
checkoutSaga(),
userSaga()
])
}
Let’s first focus on the things that might jump out at you.
rootSaga is our base saga in the chain. It is the saga that gets passed to sagaMiddleware.run(rootSaga)
. menuSaga, checkoutSaga, and userSaga are what we call slice sagas. Each handles one section (or slice) of our saga tree.
all()
is what redux-saga refers to as an effect creator. These are essentially functions that we use to make our sagas (along with our generator functions) work together. Each effect creator returns an object (called an effect) that is used by the redux-saga middleware. You should note the naming similarity to Redux actions and action creators.
all()
is an effect creator, which tells the saga to run all sagas passed to it concurrently and to wait for them all to complete. We pass an array of sagas that encapsulates our domain logic.
Watcher Sagas
Now let’s look at the basic structure for one of our sub-sagas.
import { put, takeLatest } from 'redux-saga/effects'
function* fetchMenuHandler() {
try {
// Logic to fetch menu from API
} catch (error) {
yield put(logError(error))
}
}
function* menuSaga() {
yield takeLatest('FETCH_MENU_REQUESTED', fetchMenuHandler)
}
Here we see our menuSaga, one of our slice sagas from before. It’s listening for different action types that are dispatching to the store. Watcher sagas listen for actions and trigger handler sagas.
We wrap the body of our handler functions with try/catch blocks so that we can handle any errors that might occur during our asynchronous processes. Here we dispatch a separate action using put() to notify our store of any errors. put() is basically the redux-saga equivalent of the dispatch method from Redux.
Let’s add some logic to fetchMenuHandler.
function* fetchMenuHandler() {
try {
const menu = yield call(myApi.fetchMenu)
yield put({ type: 'MENU_FETCH_SUCCEEDED', payload: { menu } ))
} catch (error) {
yield put(logError(error))
}
}
We are going to use our HTTP client to make a request to our menu data API. Because we need to call a separate asynchronous function (not an action), we use call(). If we needed to pass any arguments, we would pass them as subsequent arguments to call() — i.e. call(mApi.fetchMenu, authToken). Our generator function fetchMenuHandler uses yield to pause itself while it waits for myApi.fetchMenu to get a response. Afterwards, we dispatch another action with put() to render our menu for the user.
OK, let’s put these concepts together and make another sub-saga — checkoutSaga.
import { put, select, takeLatest } from 'redux-saga/effects'
function* itemAddedToBasketHandler(action) {
try {
const { item } = action.payload
const onSaleItems = yield select(onSaleItemsSelector)
const totalPrice = yield select(totalPriceSelector)
if (onSaleItems.includes(item)) {
yield put({ type: 'SALE_REACHED' })
}
if ((totalPrice + item.price) >= minimumOrderValue) {
yield put({ type: 'MINIMUM_ORDER_VALUE_REACHED' })
}
} catch (error) {
yield put(logError(error))
}
}
function* checkoutSaga() {
yield takeLatest('ITEM_ADDED_TO_BASKET', itemAddedToBasketHandler)
}
When an item is added to the basket, you can imagine that several checks and verifications need to be made. Here we are checking if the user has become eligible for any sales from adding an item or if the user has reached the minimum order value needed to place an order. Remember, Redux Saga is a tool for us to handle side effects. It shouldn’t necessarily be used to house the logic which actually adds an item to the basket. We would use a reducer for that, because this is what the much simpler reducer pattern is perfectly fitted to do.
Image for post
High level visual of the saga flow
We are making use of a new effect here — select(). Select is passed a selector and will retrieve that piece of the Redux store, right from inside our saga! Note that we can retrieve any part of the store from within our sagas, which is super useful when you depend on multiple contexts within one saga.
What’s a selector? A selector is a common design pattern utilized in Redux where we create a function which is passed the state and simply returns a small piece of that state. For example:
const onSaleItemsSelector = state => state.onSaleItems
const basketSelector = state => state.basket
const totalPriceSelector = state => basketSelector(state).totalPrice
Selectors serve as a reliable and consistent way to reach in and grab a piece of our global state.
Conclusion
Redux Saga is an excellent framework for managing the various changes and side effects that will occur in our applications. It offers very useful helper methods, called effects, which allow us to dispatch actions, retrieve pieces of the state, and much more.
Top comments (0)