Redux-Thunk vs Redux-Saga

Hayk Yaghubyan
Jan 20 · 4 min read

These days all dynamic web applications need to use asynchronous tasks. As a react developer you might use Redux-Thunk within your career and feeling uncomfortable to move another solution like Redux-Saga which becomes trending the last few years. Yes, it has different syntax and when you check it for the first time it looks quite confusing and doesn’t make sense.

The benefit of Redux-Saga in comparison to Redux-Thunk is that you can avoid callback hell. Also, you can more easily test your asynchronous data flow.

Redux-Thunk, however, is great for small projects and for developers who just entered into the React ecosystem. The thunks’ logic is all contained inside of the function. More than that you don’t need to learn that strange and different syntax that comes with Redux-Saga. However, in this tutorial, I will show you how can you easily move from Redux-Thunk into Redux-Saga and will explain that strange syntax :D.

Let’s install some dependencies first.

npm i --save react-redux redux redux-logger redux-saga redux-thunk

Next, we need to set up a Redux in our project. Let’s create a Redux folder and then store.js file inside it.

import { createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
import rootReducer from './root-reducer';const middlewares = [thunk];if (process.env.NODE_ENV === 'development') {
middlewares.push(logger);
}
export const store = createStore(rootReducer, applyMiddleware(...middlewares));export default store;

then we need to create root-reducer.js

import { combineReducers } from 'redux';
import fetchTasksReducer from './reducers/fetchTasksReducer'
const rootReducer = combineReducers({
tasks: fetchTasksReducer,
});
export default rootReducer;

Don’t forget to import our store inside App.js

import React, { Component } from 'react';
import { BrowserRouter, Route } from 'react-router-dom';
import { Provider } from 'react-redux';
import Tasks from './components/tasks';
import './App.css';
const store = require('./reducers').init();class App extends Component {
render() {
return (
<Provider store={store}>
<BrowserRouter>
<div className='App'>
<div className='container'>
<Route exact path='/' component={Tasks} />
</div>
</div>
</BrowserRouter>
</Provider>
);
}
}
export default App;

Now we need to create a fetchTasksReducer.

...const INITIAL_STATE = {
tasks: null,
isFetching: false,
errorMessage: undefined
};
const fetchTasksReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case "FETCH_TASKS_START":
return {
...state,
isFetching: true
};
case "FETCH_TASKS_SUCCESS":
return {
...state,
isFetching: false,
tasks: action.payload
};
case "FETCH_TASKS_ERROR":
return {
...state,
isFetching: false,
errorMessage: action.payload
};
default:
return state;
}
};
export default fetchTasksReducer;

Whenever we dispatch an action, that action goes through fetchTasksReducer and it updates our state as required. Here is how these 3 conditions work:

  • FETCH_TASKS_START: HTTP request has been started, this can be a great time to show a spinner for example that lets users know that there are some processes are going.
  • FETCH_TASKS_SUCCESS: HTTP request was successful, we must update the state.
  • FETCH_TASKS_ERROR: HTTP request has failed. You can show an error component to let the user know about the problem.

Redux-thunk

Currently, our app does not work as expected, as far as we need an action creator, to fire an action that our reducer handles.

export const fetchTasksStarted = () => ({
type: "FETCH_TASKS_START"
});
export const fetchTasksSuccess = tasks => ({
type: "FETCH_TASKS_SUCCESS",
payload: tasks
});
export const fetchTasksError = errorMessage => ({
type: "FETCH_TASKS_ERROR",
payload: errorMessage
});
const fetchTasks = () => async dispatch => {
dispatch(fetchTasksStarted())
try{
const TaskResponse = await fetch("API URL")
const task = await taskResponse.json()
dispatch(fetchTasksSuccess(tasks))
}catch(exc){
dispatch(fetchTasksError(error.message))
}
}

fetchTasks might look strange at the beginning, but it's a function that returns another function that has a parameter dispatch. Once dispatch gets called, the control flow will move to the reducer to decide what to do. In the above case, it only updates the application state, if the request has been successful.

Redux-saga

Redux-saga is a redux middleware that allows us to easily implement asynchronous code with Redux. It’s the most popular competitor for Redux Thunk.

Let’s get started. Let’s assume we have the same project but before Redux-Thunk implementation. Let’s implement Redux-saga middleware in Store.js

import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import logger from 'redux-logger';
import rootReducer from './root-reducer';import { watchFetchTasksSaga } from './saga/fetchTasks.saga';const sagaMiddleware = createSagaMiddleware();const middlewares = [logger, sagaMiddleware];export const store = createStore(rootReducer, applyMiddleware(...middlewares));sagaMiddleware.run(watchFetchTasksSaga);export default store;

It’s time to create actions:

export const fetchTasksStarted = () => ({
type: "FETCH_TASKS_START"
});
export const fetchTasksSuccess = tasks => ({
type: "FETCH_TASKS_SUCCESS",
payload: tasks
});
export const fetchTasksError = errorMessage => ({
type: "FETCH_TASKS_ERROR",
payload: errorMessage
});

Let’s create a saga folder and in the saga folder, create a new file called fetchTasks.saga.

import { takeLatest, put } from "redux-saga/effects";function* fetchTasksSaga(){
try {
const taskResponse = yield fetch("API URL")const tasks = yield taskResponse.json()yield put(fetchTasksSuccess(tasks));} catch (error) {yield put(fetchTasksError(error.message));
}
}
export default function* watchFetchTasksSaga(){
yield takeLatest("FETCH_TASKS_START", fetchTasksSaga)
}

Those function* thingies are called generator functions.
You can use both takeLatestand takeEvery but we used takeLatest

as far as it takes only the last event.

By calling the put function we can fire actions just like we did with dispatch. It allows adjusting our reducer to handle our actions.

...const INITIAL_STATE = {
tasks: null,
isFetching: false,
errorMessage: undefined
};
const fetchTasksReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case "FETCH_TASKS_START":
return {
...state,
isFetching: true
};
case "FETCH_TASKS_SUCCESS":
return {
...state,
isFetching: false,
tasks: action.payload
};
case "FETCH_TASKS_ERROR":
return {
...state,
isFetching: false,
errorMessage: action.payload
};
default:
return state;
}
};
export default fetchTasksReducer;

Conclusion

That’s it. Now you are familiar with both approaches of working with asynchronous tasks in React and Redux. You can decide which one to choose based on the project that you are working on.

Hayk Yaghubyan

Written by

Senior full-stack developer working in ReactJS, Angular,Vue.js, Node.js, and other modern technologies. Excited about web technologies.

JavaScript in Plain English

Learn the web's most important programming language.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade