From Redux Thunk to Redux Saga

From Redux Thunk to Redux Saga

·

8 min read

Many of you who are reading this probably has some experience with using Redux Thunk, know what it does, and perhaps thinking of switching to Saga, so just for a quick refresher I'll briefly list out the following facts about Redux and Redux Thunk:

  • With a plain basic Redux store, you can only do simple synchronous updates by dispatching an action
  • Redux Thunk is a middleware that allows you to write asynchronous logic that interacts with the store
  • Specifically, we can now write action creators that return a function instead of an action. The function is called a thunk and can be used to delay the dispatch of an action, or to dispatch only if a certain condition is met

Ok, so Redux Thunk works well then why do we want to choose Redux Saga instead? As with any decisions, there are always pros and cons associated with each and I think the main reason you would choose Thunk over Saga is for quick and simple projects as setting up Thunk is much easier. Besides that, Saga is almost always the way to go.

Why Redux Saga?

As quoted from the official documentation, "Redux Saga is a library that aims to make application side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) easier to manage, more efficient to execute, easy to test, and better at handling failures"

For scalability reasons, all the features mentioned above is what makes Saga superior to Thunk especially when it comes to testing. While testing thunks is not impossible, it requires mocking API calls and other functions that are used within the thunks, resulting in less readable and messier code.

Furthermore, while Redux Thunk uses callback functions to allow us to manage asynchronous logic, Redux Saga is like a separate thread in your application that manages it instead. As threads can be started, paused and cancelled from the main application, this essentially means we have full control over our Redux application state.

Refactoring from Thunk to Saga

To refactor from Thunk to Saga, let's take a look and better understand how Saga works first.

Ok, so suppose we want to have some button that request some data from an API when clicked.

1. Dispatch Action

fetchData() {
    const { data } = this.props
    dispatch({type: 'FETCH_REQUESTED', payload: data})
  }

2. Create a Saga that watches for all FETCH_REQUESTED actions that will trigger an API call to fetch the data

// sagas.js
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import api from '...'

// worker Saga: will be fired on FETCH_REQUESTED actions
function* fetchData(action) {
   try {
      const data = yield call(api.fetchData, action.payload.data);
      yield put({type: "FETCH_SUCCEEDED", payload: data});
   } catch (e) {
      yield put({type: "FETCH_FAILED", message: e.message});
   }
}

/*
  Starts fetchData on each dispatched `FETCH_REQUESTED` action.
  Allows concurrent fetches of data.
*/
function* mySaga() {
  yield takeEvery("FETCH_REQUESTED", fetchData);
}

/*
  Alternatively you may use takeLatest.

  Does not allow concurrent fetches of user. If "FETCH_REQUESTED" gets
  dispatched while a fetch is already pending, that pending fetch is cancelled
  and only the latest one will be run.
*/
function* mySaga() {
  yield takeLatest("FETCH_REQUESTED", fetchData);
}

export default mySaga;

Some notes to understand the above code better:

  • The yield keyword pauses the generator function execution and the value of the expression following the yield keyword is returned to the generator's caller
  • call(api.fetchData, action.payload.data) calls the fetchData (asynchronous) function which accepts action.payload.data as an argument
  • The result becomes similar to await [promise]
  • put instructs the middleware to dispatch the action accordingly
  • Notice that the difference between takeEvery and takeLatest (this is what makes Redux Saga so powerful!)

Now that we understand how Saga works in general, let's see how we can refactor from Thunk to Saga.

Using Thunk, our actions file would contain functions that look something like:

// actions/index.js
import axios from "axios";

export const createUser = data => {
  return (dispatch) => {
    axios
      .post("/user", {
        email: data.email,
        name: data.displayName,
      })
      .then(res => {
        return res.data;
      })
      .then(data => {
        dispatch({ type: "USER_DATA", payload: data });
      })
      .catch((err) => {
        dispatch({type: "USER_DATA_ERROR", err});
      });
  };
};

Here, I'm using a single function as an example from a previous project; just focus on the structure.

Now, to refactor our code to Saga, we can do the following steps.

Refactor Our Actions

  1. Refactor API calls to a separate file as promises

  2. Refactor functions to return a plain action object instead of thunks (i.e., callback functions) with an action type that informs an api call is requested (here, I'm calling it CREATE_USER_DATA), and then export out all the different action types.

export const CREATE_USER_DATA = 'CREATE_USER_DATA';
export const USER_DATA = 'USER_DATA';
export const USER_DATA_ERROR = 'USER_DATA_ERROR';

export const createUser = data => {
    type: "CREATE_USER_DATA",
    payload: { data.email, data.displayName }
}

Create Sagas

Create a new file for our Sagas.

import { call, put, takeLatest } from 'redux-saga/effects';
import api from '/api';
import * as actions from '/actions';

// worker Saga
function* createUser(data) {
  try {
    const response = yield call(api.createUser, data);
    yield put({ type: actions.USER_DATA, data: response.data });
  } catch (e) {
    console.error(e);
    yield put({ type: actions.USER_DATA_ERROR, error: e.message });
  }
}

// watcher Saga
function* mySaga() {
  yield takeLatest(actions.ADD_USER_DATA, createUser);
}
export default mySaga;

The result is that every time we want to create a new user data, we simply write:

dispatch(createUser());

And Redux Saga will do its thing! Last but not least, we need to apply the Saga middleware.

Applying the Saga Middleware

Currently, this is what my code looks like using thunk:

import thunk from "redux-thunk";
import reducers from "./reducers";
const store = createStore(reducers, applyMiddleware(thunk));

To apply Saga, we first create the middleware and replace it with thunk. Note that using both Saga and Thunk is possible; they are not mutually exclusive but it'll require another configuration.

const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducers, applyMiddleware(sagaMiddleware));

And we're done! That wasn't too hard wasn't it? Hopefully, this article was helpful for those transitioning from Thunk to Saga. Thanks for reading!