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 !!!