Redux Saga With Example

Redux Saga With Example

Disclaimer: I assume you know Redux in this blog. (If you don't know you can read this blog)

Redux Saga is a middleware of Redux. Now, What is the middleware of Redux? The middleware of Redux is the extension between dispatching an action and the reducer. You maybe listen about some redux middleware like a logger, redux-thunk, redux-dev-tool, and many more.

Redux Saga makes it easier to manage asynchronous things like data fetching. Redux Saga is very effective, easy to manage, easy to test, and more readable compared to Redux-Thunk.

Now we should know about the generator function first. How generator function is different from the normal function? A regular function is executed based on the run-to-completion model. It cannot pause midway and then continues from where it paused. But the generator function can do it. Let's explore it with an example.

normal function

function normal() {
    console.log('I');
    console.log('am');
    console.log('normal function');
}

normal();

When we invoked normal function then it printed all contents one after one. It can't take pause.

generator function

function* generate() {
    console.log('1 print');
    yield 1;
    console.log('2 print');
    yield 2;
}

The generator function starts with function*. Now call the generator function. The output will be nothing. Now call this function like this,

let gen = generate();

gen.next()

Now the output will be 1 print. Try to what happened here. Here yield paused the execution. This is the reason, that only 1 print is printed. yield is triggered by the next. Now call gen.next() again, then 2 print is printed.

Now follow this code,

let gen = generate();
console.log(gen.next());

The output is

1 print
{ value: 1, done: false }

The generator function returns two properties. value and done. done is determined whether more yield exists or not. If yield exists then the done value is false. Try this code,

let gen = generate();

console.log(gen.next());
console.log(gen.next());
console.log(gen.next());

The output is,

1 print
{ value: 1, done: false }
2 print
{ value: 2, done: false }
{ value: undefined, done: true }

Hopefully, you understand why the value of done is true in the last one.

This is the time to jump into Redux Saga.

Redux Saga is commonly used for fetching data asynchronously. Now set up the Redux Saga.

store.js

import { createStore, applyMiddleware, combineReducers } from 'redux';
import createSagaMiddleware from 'redux-saga';
import logger from 'redux-logger';

import userReducer from './reducer';
import { rootSaga } from '../saga/rootSaga';


const rootReducer = combineReducers({
    user: userReducer,
});

const sagaMiddleware = createSagaMiddleware();
const middleware = [sagaMiddleware, logger];

const store = createStore(rootReducer, applyMiddleware(...middleware));

sagaMiddleware.run(rootSaga);

export default store;

In the store file, we have to declare sagaMiddleware and connect it to the store. sagaMiddleware.run(rootSaga) starts the saga middleware.

Create a saga folder then create more two folders helper and request in this. Create a file name rootSaga. (File structure is optional, you can create as your wish. I just give you the idea to clean your code)

I will write the saga function in the helper folder and APIs in the request folder.

Before starting, you should know about some effects of Redux-saga

  • select: returns the full state of the application
  • put: dispatch an action into the store
  • call: run something
  • cancel: cancels the saga execution
  • fork: run multiple sagas parallelly.
  • all: run multiple Effects in parallel and wait for all of them to complete
  • takeEvery: takes every matching action and runs the given saga
  • takeLatest: takes every matching action and runs the given saga, but cancels every previous saga that is still running

Redux-setup

  • Action Type

    export const Loading = "loading";
    export const Success = 'success';
    export const Error = 'error';
    
  • Action


import * as types from './type';


export const loadingStart = () => {
    return {
        type: types.Loading,
    }
}

export const userSuccess = (user) => {
    return {
        type: types.Success,
        payload: user,
    }
}

export const userError = (error) => {
    return {
        type: types.Error,
        payload: error,
    }
}
  • Reducer
import * as types from './type'
const initialState = {
    users: [],
    loading: false,
    error: null,
}

const userReducer = (state = initialState, action) => {
    switch (action.type) {
        case types.Loading:
            return {
                ...state,
                loading: true,
            }
        case types.Success:
            return {
                ...state,
                users: action.payload,
                loading: false,
            }
        case types.Error:
            return {
                ...state,
                loading: false,
                error: action.payload,

            }
        default:
            return state;
    }
}

export default userReducer;

Redux-Saga Set up

Now create userHandle file in the helper folder.

import * as types from '../../redux/type';
import { takeEvery, put, call, takeLatest, take, delay } from 'redux-saga/effects';
import { userSuccess, userError} from '../../redux/action';
import { loadUserApi } from '../request/api';

export function* userSaga() {
    try {
        const response = yield call(loadUserApi);
        yield put(userSuccess(response.data));
    } catch (error) {
        yield put(userError(error));
    }
}

export function* onLoadUser() {
    yield takeEvery(types.Loading, userSaga)
}

Here, when Loading matches in onLoadUser function, it calls userSaga to execution. When we get response, connected this to the userSucess or userError action.

You usually listen to two keywords associated with redux-saga. i) Worker Saga ii) Watcher Saga.

Watcher Saga: Watcher Saga observes all dispatched actions. If any match, then it assigns this to the worker Saga.

This part is the watcher saga,

export function* onLoadUser() {
    yield takeEvery(types.Loading, userSaga)
}

Worker Saga: Worker Saga executes all the functionality and side effects.

export function* userSaga() {
    try {
        const response = yield call(loadUserApi);
        yield put(userSuccess(response.data));
    } catch (error) {
        yield put(userError(error));
    }
}

Now create a file in the request folder and write code for fetching API

import axios from "axios";

export const loadUserApi = async () => {
    const response = await axios.get("http://localhost:3004/users");
    return response;
}
`

Now write code in rootSaga file which is connected to the store.

import { all, fork } from 'redux-saga/effects';
import { onLoadUser, addUser, deleteUser, updateUser } from './helper/userHandler';

// fork is used to run the function parallel
// can add multiple sagas in this array
const Sagas = [fork(onLoadUser)];

export function* rootSaga() {

    yield all([...Sagas])

}

rootSaga always listens to all sagas. All this process is asynchronous.

Hopefully, You get the idea about Redux Saga.

For Coding Reference: Github

Happy Coding !!!