Using Redux-Saga to handle side-effects and testing it using Jest

Gethyl George Kurian
5 min readJan 10, 2017

--

When I started looking into Redux-Saga, I felt as thought I tripped into a cave and there was no escape even though I could see the light at the distant top.

This was mainly because I found it really confusing initially to grasp ES6 Generators, which is used to build Redux-Saga.But once I understood how to redux-saga, I am loving it.

The intention of this post is to help you understand how to use redux-saga. To get started with it, I would advice you to read the official document first to understand how you can use it as a middleware to handle side-effects and also try to understand the basics of how ES6 Generators work.

I have put across a small example on how to use Redux-Saga here, and also I use Jest to test the sagas.

If you aren’t familiar with Jest, you can check this post I have written on how to test a React-Redux app using Jest.

This is how the basic screen looks before and after I press the button.

Before clicking on the Button
After clicking on the Button

Lets not look too much into the details of the screen for now. But we will come to that in the subsequent sections.

Setting up

The easiest way start working with the example I have provided is to

  1. Clone the project into your local folder
git clone https://github.com/Gethyl/FirstReduxSagaExample

2. Change to that folder.

cd ./FirstReduxSagaExample

3. Install the dependencies and devDependencies by issuing yarn install or npm install

I would suggest you to learn and start using yarn over npm. yarn was created to overcome the shortfalls of npm and therefore I would recommend you to start exploring it. Also maybe you can read this article to get convinced :)

Running The Example

If you check the script in package.json I have added

"dev": "webpack-dev-server --content-base --inline --hot"

So to run your app, all you have to do is

yarn run dev

And go to http://localhost:8080/ and see the page.

Understanding how Redux-Saga works

Now that you have the app running,

Before clicking on the Button

We will be printing to the screen this.props.output so that it’s easy for you to understand the flow of the app.

When the screen is rendered initially, you can see that we put initial state of the app in the list.

initialState = {output:"@@redux/INIT action triggered"}

Next, click on the button Click to Fetch Async Data, and you will see that the list has few more messages.

These messages are useful in understanding how our app works.

After clicking on the Button

Now let us look at each of the messages to understand when they were triggered.

To keep things simple, the reducer always returns

return {…state,output:action.output}

1. ASYNC_TEST_SAGA triggered from TestContainer.js

When the button is clicked, we dispatch the action ASYNC_TEST_SAGA

//TestContainer.jsdispatch(asyncTestSaga("ASYNC_TEST_SAGA triggered from TestContainer.js"))

The actionCreator:

//middlewareActions.jsexport const asyncTestSaga = (output) => {
return {
type: "ASYNC_TEST_SAGA",
output: output //"ASYNC_TEST_SAGA triggered from Action"
}
}

And you see the message printed on the screen. There is nothing special happening here and it is not a side-effect.

But the next items which are printed are interesting, because this is where redux-saga comes in picture, as they are side-effects when we trigger the above action.

Let me display the entire sagas.js here so that it becomes easier for you in the subsequent steps.

2. ASYNC_TEST -ASYNC_TEST_SAGA triggered from TestContainer.js

Since we use Redux-Saga middle ware to capture side-effects as shown below:-

//sagas.jsexport default function* rootSaga (){  
yield takeEvery("ASYNC_TEST_SAGA",testFunction0)
}

What this does it, every time, we click the button and when ASYNC_TEST_SAGA action is fired, as we catch it for side-effects using Redux-Saga and call the function testFunction0 .

takeEvery will pass the action as the third parameter. If you explicitly pass arguments, then action will be appended to the end of the arguments.

This is why testFunction0 receives action as parameter.

export  function* testFunction0 (action){
yield call(testFunction1,action.output)
...
}

This will in turn call, testFunction1

export  function* testFunction1 (sagaPut){
yield call(delay, 1000)
yield put({type: 'ASYNC_TEST', output:"ASYNC_TEST -" + sagaPut})
...
}

Here you can see that we are prefixing “ASYNC-TEST -“ to the output passed, and call the action ASYNC_TEST.

Notice here, we are directly calling an action and not an actionCreator. Though this is possible and I have used it just to show you guys, I would strictly advice to use actionCreators to maintain consistency.

3. ASYNC_TEST_INITIAL Action Creator!!

Next, moving to the next line in testFunction1

export  function* testFunction1 (sagaPut){
...
yield put(asyncTestInitial())
...
}

It call asyncTestInitial actionCreator.

export const asyncTestInitial = () => {
return {
type: "ASYNC_TEST_INITIAL",
output: "ASYNC_TEST_INITIAL Action Creator!!"
}
}

4. This action is not present in ActionCreator

Next, moving to the last line in testFunction1

export  function* testFunction1 (sagaPut){
...
yield put({type: 'ASYNC_TEST_NOACTIONCREATOR', output:"This action is not present in ActionCreator"})
}

You can see again, we are directly using action without an actionCreator. Though it is possible, please use actionCreators through out the app for consistency.

5. ASYNC_FETCH_INITIAL action triggered!!

Now that all the lines intestFunction1 are executed, we execute the next line intestFunction0

export  function* testFunction0 (action){
...
yield call(delay,1000)
yield put(asyncFetchInitial())
...
}

Having actionCreator:

export const asyncFetchInitial = () => {
return {
type: "ASYNC_FETCH_INITIAL",
output: "ASYNC_FETCH_INITIAL action triggered!!"
}
}

6. ASYNC_FETCH_SUCCESS Fetch was successfull!!

And finally we make fetch testdata.json and display the field dummyOutput

export  function* testFunction0 (action){
...
try {
const dummyOutput = yield call(FetchTestData)
yield put(asyncFetchSuccess(dummyOutput))
} catch (error) {
console.log("Error in fetch" + error)
yield put(asyncFetchError())
}
}

FetchTestData:

export function FetchTestData(){
return fetch('testdata.json')
.then((res)=> res.json())
.then((jsondata)=> {
const {dummyOutput} = jsondata.dummy
return dummyOutput
})
}

Having actionCreator:

export const asyncFetchSuccess = (dummyOutput) => {
return {
type: "ASYNC_FETCH_SUCCESS",
output: "ASYNC_FETCH_SUCCESS ".concat(dummyOutput)
}
}

Running Tests

If you check the script in package.json I have added ”test”: “jest”

So to run the test all you have to do is

yarn test

And Jest will pick all the test files which are put under the folder __test__.

Conclusion

As you can see how the items are build in the list, I hope this will give you a clear idea on how the side-effects are captured by Redux-Saga.

This is just an extremely simple example on how to use Redux-Saga, and it can do much more complex side-effects.

As always …. Happy Coding!! :-)

--

--