I wanted to try redux-saga in one of my learning projects but I got confused about how generators are used to do async operations. First I had to understand them so I used generators in my project. Now that I have an overview about them, it’s time to move on redux-saga.

Before going into redux-saga code, I’ll extract relevant usages of react, redux and redux-thunk from my project.

Current implementation with redux-thunk

Dispatch action

CurrentReadingSessionComponent.js

import { fetchBookAction } from 'actions/BookAction';

retrieveBook() {
    this.props.dispatch(fetchBookAction(this.props.bookUuid))
}

Create action

BookAction.js

import { fetchBook } from 'api/BookApi';
import { receiveMessageAction } from 'actions/MessageAction';

export const RECEIVE_BOOK = 'RECEIVE_BOOK';

export function fetchBookAction(uuid) {
    return function (dispatch) {
        fetchBook(uuid)
            .then(response => dispatch(receiveBookAction(response.data)))
            .catch(error => dispatch(receiveMessageAction(error)));
    }
}

export function receiveBookAction(book) {
    return {
        type: RECEIVE_BOOK,
        payload: book
    }
}

fetchBookAction function is the place where redux-thunk is used so that instead of returning a simple action object, it returns another function that receives dispatch function as the first parameter. In this way redux-thunk delays the dispatch of the original action by first getting the data asynchronous and only when data is ready we dispatch another action with the result.

Fetch data

BookApi.js

import axios from 'axios';
import user from 'User';
import localizer from 'utils/Localizer';

export const BOOKS_ENDPOINT = `/users/${user.id}/books`;

export function fetchBook(uuid) {
    return new Promise((resolve, reject) => {
        axios.get(`${BOOKS_ENDPOINT}/${uuid}`)
            .then(response => resolve(response))
            .catch(error => reject(localizer.localize('book-retrieve-error', error.response.status)))
    });
}

Update state

BookReducer.js

import { RECEIVE_BOOK } from 'actions/BookAction';

export function book(book = null, action) {
    switch(action.type) {
        case RECEIVE_BOOK:
            return action.payload;
        default:
            return book;
    }
}

Create the store

LibraryRouter.js

import {
    createStore,
    applyMiddleware
} from 'redux';
import { currentReadingSessionReducer }  from 'reducers/CurrentReadingSessionReducer';
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension';

this.store = createStore(currentReadingSessionReducer, composeWithDevTools(applyMiddleware(thunk)));

Connect the store to react component

CurrentReadingSessionComponent.js

import { connect } from 'react-redux';

const mapStateToProps = state => {
    return state
};

export default connect(mapStateToProps)(CurrentReadingSessionComponent);

Use redux-saga instead of redux-thunk

In order to follow easier the changes, I’ll extract relevant code samples after adding redux-saga in this project. Please note that I’ll focus only in the updates, code that did not change will not be added again.

Create action

BookAction.js

export const FETCH_BOOK = 'FETCH_BOOK';
export const RECEIVE_BOOK = 'RECEIVE_BOOK';

export function fetchBookAction(uuid) {
    return {
        type: FETCH_BOOK,
        payload: uuid
    }
}

export function receiveBookAction(book) {
    return {
        type: RECEIVE_BOOK,
        payload: book
    }
}

Async calls are not started anymore on action dispatch. fetchBookAction(action) returns a plain action object.

Watch for fetch requests with redux-saga

BookSagas.js

import { call, put, takeLatest } from 'redux-saga/effects';
import { fetchBook } from 'api/BookApi';
import { receiveBookAction, FETCH_BOOK } from 'actions/BookAction';
import { receiveMessageAction } from 'actions/MessageAction';

export function* watchFetchBook() {
    yield takeLatest(FETCH_BOOK, callFetchBook);
}

function* callFetchBook(action) {
    try {
        const bookUuid = action.payload;
        const response = yield call(fetchBook, bookUuid);
        yield put(receiveBookAction(response.data));
    } catch(error) {
        yield put(receiveMessageAction(error));
    }
}

The main change is here, in the new added saga functionality. In redux-thunk version this functionality was implemented with promises chaining. Here we have two generator functions:

  • watchFetchBook uses takeLatest to monitor for FETCH_BOOK action. This is the watcher saga.
  • callFetchBook saga will actually do the work and that’s why is called the worker saga.

Use a root saga

RootSagas.js

import { all, call } from 'redux-saga/effects';
import { watchFetchBook } from 'sagas/BookSagas';

export default function* rootSaga() {
    yield all([
        call(watchFetchBook)
    ]);
}

This is similar with the root reducer and is needed to start watching for dispatched actions that trigger async calls.

Create the store

LibraryRouter.js

import {
    createStore,
    applyMiddleware
} from 'redux';
import { currentReadingSessionReducer }  from 'reducers/CurrentReadingSessionReducer';
import createSagaMiddleware from 'redux-saga';
import rootSaga from 'sagas/RootSagas';
import { composeWithDevTools } from 'redux-devtools-extension';

const sagaMiddleware = createSagaMiddleware();

this.store = createStore(currentReadingSessionReducer, composeWithDevTools(applyMiddleware(sagaMiddleware)));

sagaMiddleware.run(rootSaga);

redux-saga middleware replaces corresponding redux-thunk middleware. Here is the place where rootSaga is started.

Conclusion

I see two improvements of using redux-saga instead of redux-thunk:

  • The start of async calls are separated from actions dispatch.
  • Async code is easier to read in this way, at least for me.