Getting Started With Redux-Saga

Redux saga is a redux middleware which makes application side effects more manageable and testable. These side effects in simpler terms are just apis for data fetching or accessing browser cache which are unavoidable important parts of every app.

I used another popular middleware called Thunk for handling these kind of side effects and it did make my life easy. Switching from thunk to saga was not that straight forward at first as it introduced some new concepts and patterns. So this article is for people who are doing the same or at least have a solid understanding of how react works with redux. I will try to make basic redux saga concepts simpler to understand as a lot of well written and detailed articles on the web are a bit daunting to begin with.

By default, In a plain redux store only synchronous updates are possible with actions that dispatch objects. For asynchronous updates, we need middlewares like Saga or Thunk.

Thunk cleverly allows us to dispatch functions instead of objects which handle our async code and then eventually dispatch objects to the store. I enjoyed this approach until some of our action creators were huge with a lot of promise chains. Our actions became difficult to test and understand as they were no longer pure.

Sagas setup a generator function which intercepts the dispatched action object, pauses and does a network request, waits for it to complete and when done dispatches another action with the desired data. I know its a pretty long statement but I hope it will be clear in a few minutes. Let’s start with generators.

Generators are functions with a star (asterisk) attached to them and were part of Javascript ecosystem since Es6. In a generator function, the function execution can be delayed. They don’t run to completion after they are called like normal functions do but instead can stop midway and continue and stop again and continue again. These are normally built with yield expressions, where each yield is a stopping point. The syntax kind of looks like async/await where a async function can have multiple awaits.

Let’s look at our first saga example where we are fetching a list of authors and saving them to our store.

export function fetchAuthors() {
  return {
    type: 'FETCH_AUTHORS'
  }
}

export function saveAuthorsToList(authors) {
  return {
    type: 'SAVE_AUTHORS_TO_LIST',
    payload: authors
  }
}

With Thunk we would have a single function which would handle our asynchronous fetching of authors and then would save that data to the store using a single action object. But with saga we will do it in a different way.

Here we have two functions defined called fetchAuthors and saveAuthorsToList. Instead of having a single big function we are breaking it into two simple functions which are our action creators. These action creators dispatch pure redux actions which can be easily tested.

Now we need our redux saga code which will look something like this.

import { call, put, takeEvery } from 'redux-saga/effects';

import { saveAuthorsToList } from './actions';
import Api from './apis';

function* fetchAuthorsFromApi() {
  yield takeEvery('FETCH_AUTHORS', makeAuthorsApiRequest);
}

function* makeAuthorsApiRequest(){
  const authors = yield call(Api.requestAuthors);
  yield put(saveAuthorsToList(authors));
}

export default fetchAuthorsFromApi();

You can see two star attached functions in the above example, fetchAuthorsFromApi and makeAuthorsApiRequest, which are our generator functions. These are our wizards and handle all saga magic

The first generator, fetchAuthorsFromApi has one yield statement so this function has one stopping point where it pauses before its completion. It uses a predefined redux saga effect called takeEvery.

takeEvery calls a generator function whenever a dispatched action matches the specified pattern or type.

Our generator function fetchAuthorsFromApi runs in the background and the statement

yield takeEvery(‘FETCH_AUTHORS’, makeAuthorsApiRequest)

will intercept every action dispatched with type FETCH_AUTHORS and calls another generator function called makeAuthorsApiRequest which will handle our async api call. HenceFETCH_AUTHORS type action won’t be handled in a reducer like other normal actions and instead will be intercepted by our saga.

Inside makeAuthorsApiRequest generator we have two yield statements. The first one,

const authors = yield call(Api.requestAuthors)

uses another saga effect called call which like its name, calls a function with or without arguments. In our case it calls a function which has an api request to fetch our authors. It is better if we have a separate file for all our functions with api calls. The generator function’s execution here is paused until our api call finishes and we have our response saved in authors.

Now after we have our authors data the first yield state is done and our second yield statement gets executed which is

yield put(saveAuthorsToList(authors))

This statement uses another predefined redux saga effect called put which is used to dispatch our redux actions. So we call our second action creator saveAuthorsToList with the desired payload which here is our authors data.

So the saga magic ends here and our normal redux flow continues. We will have a reducer to handle SAVE_AUTHORS_TO_LIST action in the store which will take the authors payload and returns the state.

We can also handle any errors that our api call can return by wrapping our yield call in traditional try /catch blocks. We can dispatch a separate action to handle our error in the user state using the same put effect.

import { call, put, takeEvery } from 'redux-saga/effects';

import { saveAuthorsToList } from './actions';
import Api from './apis';

function* fetchAuthorsFromApi() {
  yield takeEvery('FETCH_AUTHORS', makeAuthorsApiRequest);
}

function* makeAuthorsApiRequest(){
  try {
    const authors = yield call(Api.requestAuthors);
    yield put(saveAuthorsToList(authors));  
  } catch (err) {
    yield put(saveAuthorToListError();
  }
}

export default fetchAuthorsFromApi();

Our store.js file is where we connect our redux-saga middleware to the store. This is an example from the official redux-saga repository.

import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';

import reducer from './reducers';
import mySaga from './saga';

// create the saga middleware
const sagaMiddleware = createSagaMiddleware();
// mount it on the Store with our reducer
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
);

// then run the saga
sagaMiddleware.run(mySaga);

Lastly hooking up saga to our store is pretty straight forward like using any other middleware. We have an extra step where we run or kickstart our saga using sagaMiddleware.run() After this our saga generator function will start running and lay low, silently waiting for any actions that need some saga magic.

So I have briefly explained one of the most common scenarios where a saga can be used. I have talked about basic but important saga effects like takeEvery, call and put . There are many other useful effects and advanced concepts as well that you can read from the official documentation. I hope this article gets you started with saga and helps you understand those other advanced concepts.

Thank you for reading 😀🙏

#Redux #React #WebDev

Getting Started With Redux-Saga
6.05 GEEK