Replacing Redux Thunks with Redux Sagas

Written by aaron.klaser | Published 2018/02/15
Tech Story Tags: redux | javascript | react | redux-saga | software-development

TLDRvia the TL;DR App

The React Blog Series: Part Five

This article is part five in a weekly series on building a blog with React and is an extension of the code created in previous parts.

The React Blog Series

Part One: Building a website with React and BulmaPart Two: Building a Blog with React and ContentfulPart Three: Import your Medium Feed into ReactPart Four: Adding a Redux to a React BlogPart Five: Replacing Redux Thunks with Redux SagasPart Six: (In Progress) Writing Unit Test for a React Blog with Redux Sagas

First of all, let me start with I am by no means an expert on this subject. I have only actually been using Sagas for the last two weeks. If you’re anything like me, as you read this, you will constantly be asking yourself, why the hell would anyone want to use this, it’s massive overkill, and you would be absolutely correct. It is overkill… for what I’m doing, but at the end of the day Sagas are better, more scalable, and infinitely easier to unit test which we will see in Part Six. If you don’t like my explanations or examples, I understand. There is also SO MUCH more to Sagas than what I’m going to use here. Here is the link to the Redux Saga documentation. Good Luck! — Aaron Klaser

What is Redux Saga?

Sagas are a redux middleware that handles asynchronous actions triggered in your application often referred to as ‘side effects’. They are constantly watching for certain actions to be dispatched, before they but in and take over controller of the situation.

Sagas are implemented as Generator functions that yield objects to the redux-saga middleware.

Wait, what is a Generator function?

!@#$ing magic, thats what!

But, heres a better explanation which I stole from Mozilla.

Generators are functions which can be exited and later re-entered. Their context (variable bindings) will be saved across re-entrances.

Calling a generator function does not execute its body immediately; an iterator object for the function is returned instead. When the iterator’s next() method is called, the generator function's body is executed until the first [yield](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield "The yield keyword is used to pause and resume a generator function (function* or legacy generator function).") expression, which specifies the value to be returned from the iterator or, with [yield*](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield* "The yield* expression is used to delegate to another generator or iterable object."), delegates to another generator function. The next() method returns an object with a value property containing the yielded value and a done property which indicates whether the generator has yielded its last value as a boolean. Calling the next() method with an argument will resume the generator function execution, replacing the yield expression where execution was paused with the argument from next().

A return statement in a generator, when executed, will make the generator done. If a value is returned, it will be passed back as the value. A generator which has returned will not yield any more values.

Okay, Back to Sagas

Now where was I? Ah yes… Sagas are implemented as Generator functions that yield objects to the redux-saga middleware. The yielded objects are a kind of instruction to be interpreted by the middleware. When a Promise is yielded to the middleware, the middleware will suspend the Saga until the Promise completes. Once the Promise is resolved, the middleware will resume the Saga, executing code until the next yield.

Redux Saga middleware does a lot of the Generator magic for us behind the scenes yielding to saga functions like put, all, call, and takeLatest.

What happens is that the middleware examines the type of each yielded Effect then decides how to fulfill that Effect. If the Effect type is a PUT then it will dispatch an action to the Store. If the Effect is a CALLthen it'll call the given function. This separation between Effect creation and Effect execution makes it possible to test our Generator in a surprisingly easy way using next()

In short, Generators are pause able functions like a music track, and Sagas are the DJ that press the pause play and stop buttons on the track.

But enough chit chat, let’s get started!

Install Saga

npm install redux-saga

Then in store/index.js let’s connect Saga. Saga is a redux middleware so we will be injected it into our redux applyMuiddleware function, but we will have to create it first.

import { createStore, applyMiddleware } from 'redux'import { rootReducer } from './rootReducer'import reduxImmutableStateInvariant from 'redux-immutable-state-invariant'import thunk from 'redux-thunk'import { composeWithDevTools } from 'redux-devtools-extension';import createSagaMiddleware from 'redux-saga'

const sagaMiddleware = createSagaMiddleware()

export function configureStore() {return createStore(rootReducer,composeWithDevTools(applyMiddleware(sagaMiddleware,thunk,reduxImmutableStateInvariant())))}

As of right now, this does nothing. We will need to initialize the sagas which need some set up before we can do that. We are going to basically gut our current thunk setups.

Creating Our Saga Helpers

We are going to use three helpers:

  1. createAsyncTypes helper, which will generate a type object with pending, success, and error.
  2. createActions helper, which will generate our actions for us.
  3. createReducer helper which we will use to replace our switch statements.

In the store folder create a file called Utilities.js

_///////////////////// ActionHelpers /////////////////////_const asyncTypes = {PENDING: 'PENDING',SUCCESS: 'SUCCESS',ERROR: 'ERROR'}

export const createAsyncTypes = typeString =>Object.values(asyncTypes).reduce((acc, curr) => {acc[curr] = `${typeString}_${curr}`return acc}, {})

export const createAction =(type, payload = {}) =>({ type, ...payload })

_///////////////////// createReducer /////////////////////_export const createReducer =(initialState, handlers) =>(state = initialState, action) =>handlers.hasOwnProperty(action.type)? handlers[action.type](state, action): state

I might revisit these in the future, I’m not 100% sold on the createReducers approach yet, but for now it works and it’s pretty clean

Moving from Thunks to Sagas

Update Blog Types

Open store/blog/types.js

Before, we only had 2 types, we used BLOG_LOADING to set the loading state for both complete and error, and LOAD_BLOG_SUCCESS to load our data on success.

--- BEFORE ---

/*** Blog Types*/export const BLOG_LOADING = 'BLOG_LOADING'export const LOAD_BLOG_SUCCESS = 'LOAD_BLOG_SUCCESS'

But now, we are going to call our new help to createAsyncTypes which will create our three types automatically; Pending (loading ), Success (load data), Error (loading complete).

--- After ---

/*** Blog Types_*/_import { createAsyncTypes } from './../Utilities'

//Using ASYNC as a convention to know that I'll have three types.export const GET_BLOG_ASYNC = createAsyncTypes('GET_BLOG')

This will make more sence in a moment.

Update Blog Reducer

Open store\blog\reducer.js

This can be done one of two ways, we could just simply update the types in the switch cases to use the new type names we just created like this.

switch (action.type) {case types.GET_BLOG_ASYNC.PENDING:return {...state,loading: true}case types.GET_BLOG_ASYNC.SUCCESS:return {...state,posts: action.posts,loading: false}case types.GET_BLOG_ASYNC.ERROR:return {...state,loading: false}default:return state}

This is an example of what our createAsyncTypes types helper has done. It generated a type object with 3 values and using the convention _ASYNC we know that GET_BLOG_ASYNC will contains PENDING, SUCCESS, and ERROR.

Let’s make this a little bit more intelligent by using out createReducer helper and passing it an object instead of a switch statement.

Delete everything and paste in this

_/*** Blog Reducer*/_import initialState from './../initialState'import { createReducer } from './../Utilities'import * as types from './types'

export default createReducer(initialState.blog, {[types.GET_BLOG_ASYNC.PENDING](state) {return {...state,loading: true}},[types.GET_BLOG_ASYNC.SUCCESS](state, action) {return {...state,posts: action.posts,loading: false}},[types.GET_BLOG_ASYNC.ERROR](state) {return {...state,loading: false}}})

Yes, I know what your thinking and I agree. This does look more confusing then a switch statement and just as clean. It actually took me a minute to understand what that object was actually doing because it wasn’t something I had ever written out like this we before. It’s creating an object that each equals and an anonymous functions but it’s setting the keys using square brackets.

obj = {key: fn,[key]fn}

Or you can use arrow functions which you will need to a colon arrow and parentheses around the object.

export default createReducer(initialState.blog, {[types.GET_BLOG_ASYNC.PENDING]:(state) => ({...state,loading: true}),[types.GET_BLOG_ASYNC.SUCCESS]:(state, action) => ({...state,posts: action.posts,loading: false}),[types.GET_BLOG_ASYNC.ERROR]:(state) => ({...state,loading: false})})

Which ever way you choice is fine, let I said I’m not 100% on board yet, but I think this can help create an async reducer creator in the future ;)

Create Our First Saga

In our store\blog folder create a file called sagas.js

_/*** Blog Sagas*/_import * as contentful from 'contentful'import { all, call, put, takeLatest } from 'redux-saga/effects'import { actions } from './../Blog'import * as types from './types'

const client = contentful.createClient({space: 'qu10m4oq2u62',accessToken: 'f4a9f68de290d53552b107eb503f3a073bc4c632f5bdd50efacc61498a0c592a'})

const fetchPosts = () => client.getEntries()

function* getBlogPosts() {try {const posts = yield call(fetchPosts)yield put(actions.success(posts.items))} catch (e) {console.log(e)yield put(actions.error(e))}}

export default function* () {yield all([takeLatest(types.GET_BLOG_ASYNC.PENDING, getBlogposts)])}

So, what is this doing? Well, let’s start with the obvious.

client is my connection to my Contentful

fetchPosts calls our to Contentful to retrieve my posts and returns them as a Promise

getBlogPosts is our actual saga. It first yields the call to fetchPosts, saga magic kicks in and pauses the function and waits for Contentful to return the data. Then, once the data has been successfully returned, instead of returning a promise it actually return the data to const posts and calls next() behind the scenes which tells the function to play again. It then calls yield put which magically dispatches the the actions with the post items. If fetchPosts() where to fail, an exception is thrown which triggers the catch.

export default function* is the action watcher that is connected to the Redux Saga middleware. This is similar to trigger the actions, except instead of the call an action and the action being dispatched to the reducer, the saga watches for that actions to be called, then it intercepts and calls the saga. takeLatest tells Redux Saga to stop any previous running saga task if still running then run a new one.

takeLatest(types.GET_BLOG_ASYNC.PENDING, getBlogPosts) says when the action types.GET_BLOG_ASYNC.PENDING is called, stop any getBlogPosts tasks previously call that my be not be completed and call a new getBlogPosts() function.

The yield all function is a kind of housing to hold all of our watchers for the blog sagas. If we have more actions, we could create more sagas and create more watchers to call those sagas, those additional watchers would go in side the all function.

We then export the default function, which creates gets injected into bound to the sagaMiddleware.run function in the rootSaga.

Setting Up the rootSaga

We need to add all our sagas to the sagaMiddleware so we can start all of our watchers when the app loads.

In the store folder, create a file called rootSaga.js

import blog from './blog/sagas'

const sagas = [blog]

export const initSagas = (sagaMiddleware) =>sagas.forEach(sagaMiddleware.run.bind(sagaMiddleware))

Right now, we only have the one saga. If we set up the Medium store with Saga’s then we would import medium and pass it in to the sagas array.

Note: we don’t need to import every function from our blog/saga.js file, only t action watchers, which we wrapped in an all function which like like a reducer for your sagas.

Actions, The New Face of Blog

Now, that we have some sagas that can watch our actions we need to update our now out of data actions.

If you have been reading along, you know that I follow the fractal file structure which in the case of the store means the store/Blog.js is my public app facing code, and everything in the store/blog folder is private to the store. Sagas aren’t called by the app, the actions are. store/Blog.js currently contains our thunks but those are not necessary anymore, so we can delete them and replace them with the actions that the app will call that will trigger our sagas. We can then remove the actions.js file from our store/blog folder.

Here is the before and after of the Blog.js file

--- Before ---

import * as contentful from 'contentful'import * as actions from './blog/actions'

const client = contentful.createClient({space: 'qu10m4oq2u62',accessToken: 'f4a9f68de290d53552b107eb503f3a073bc4c632f5bdd50efacc61498a0c592a'})

export function loadBlog() {return dispatch => {dispatch(actions.blogLoading())return client.getEntries().then(({items}) => {dispatch(actions.loadBlogSuccess(items))}).catch(error => {console.log(error)dispatch(actions.blogLoading(false))})}}

We get rid of all that logic because that the Saga’s job now, and replace it with a simple actions object that out app can call.

--- After ---

import { createAction } from './Utilities'import * as types from './blog/types'

export const actions = {pending: () => createAction(types.GET_BLOG_ASYNC.PENDING),success: (posts) => createAction(types.GET_BLOG_ASYNC.SUCCESS, { posts }),error: (error) => createAction(types.GET_BLOG_ASYNC.ERROR, { error })}

The createAction function doesn’t really do anything more then clean our code up by make it so that no matter how many things are actually passed in as the payload, this function always takes 2 arguments. I think in the future we can use this to help automate some of our async CRUD processes.

One Last Thing, Initialize The Sagas

In our store/index.js

...import createSagaMiddleware from 'redux-saga'import { initSagas } from './rootSaga'

const sagaMiddleware = createSagaMiddleware()

export function configureStore() {const store = createStore(rootReducer,composeWithDevTools(applyMiddleware(sagaMiddleware,thunk,reduxImmutableStateInvariant())))**initSagas(sagaMiddleware)

return store**}

Originally, we were return createStore, but initSagas need sagaMiddleware to be initialized first. So, we need to create the store then pass sagaMiddleware to initSages which will start all our watchers.

Instead doing something complex in the application root index to call initSagas we can just set a store variable to createStore, then call initSagas before return the store.

That’s it!

Granted, I only just barely scratched the surface of what Sagas can do, this is a good primer to getting them set up and actually working.

Let’s Review

  • We Installed Sagas
  • We created some helpers for types and reducers.
  • Updated our types
  • Updated our reducer and learned some new technics
  • Created our first saga
  • Set up a rootSage
  • Moved our actions to the public class
  • Initialized our Saga on app loaded

Yes this is way more complicated, and a fair of extra set up, and has kind of a step learning curve, and…. well you get the idea. But, it’s ultimately a better solution. It’s cleaner(ish), more scalable, more controllable, it’s real-time ready, and it’s infinitely easier to test which we will see in my next() article 😉

Next — Writing Unit Test for a React Blog with Redux Sagas


Published by HackerNoon on 2018/02/15