Best coding practice and structure for redux saga with react hooks | Separate watcher and worker saga | React clean code
Hello everyone, in this article we are going to learn about proper use of redux saga, some effects of redux saga like put, call, take, takeEvery etc. Along with that we are going to separate saga files into watcher and worker for improving code readability.
Let's create a simple react application with will render list of data on click of a button. Whenever user clicks on the button an endpoint is called and data is fetched from a remote server.
We will use a sample endpoint provided by reqres.in
Finished application looks like below screenshot :
Folder structure for this application will look like below:
components : will have all the components we will use for the app
components -> style : will have separate folder and file to hold the styling
REDUX setup :
constants -> index : will hold all action types and constant values
actions -> index : will hold all the actions
reducers -> index : is the root reducer
reducers -> others : other reducers
store -> index : store configuration
Sagas :
sagas -> index : will hold root saga or the watcher saga
sagas -> worker : all worker sagas will reside here
Let's setup the constant files first which will hold different action types(actions -> index) :
Configure the store :
But we still don't have any saga and reducers setup, so let's set them up...
For reducers we basically have three states in this scenario :
1) Data loading
2) Data fetch success
3) Data fetch error
For all these three scenarios we will handle in three different reducer files, it can be handled in one file but this dividing way makes code more readable and maintainable
So let's create them one by one,
Data Loading reducer :
Data Fetch Success reducer :
Data Fetch Error reducer :
In the rootReducer we will combine all the reducers.
Root Reducer:
All the cases are handled using reducers. So redux will automatically update the store for specific actions displayed, we need to create the saga now.Let's create dataWorkerSaga first which will fetch data from endpoint and based on success / error it will trigger specific action types
Data worker saga:
For handling endpoints using axios a separate file is created within api folder:
Watcher or root saga:
So redux setup with redux saga is completed now, let's connect our basic react hook component with redux store now
Adding some styles :
Now in App.js file we need to pass store instance via provider
And we are done, we can test the application now :
And it is working :)
thanks for your time
Let's create a simple react application with will render list of data on click of a button. Whenever user clicks on the button an endpoint is called and data is fetched from a remote server.
We will use a sample endpoint provided by reqres.in
Finished application looks like below screenshot :
Folder structure for this application will look like below:
components : will have all the components we will use for the app
components -> style : will have separate folder and file to hold the styling
REDUX setup :
constants -> index : will hold all action types and constant values
actions -> index : will hold all the actions
reducers -> index : is the root reducer
reducers -> others : other reducers
store -> index : store configuration
Sagas :
sagas -> index : will hold root saga or the watcher saga
sagas -> worker : all worker sagas will reside here
Let's setup the constant files first which will hold different action types(actions -> index) :
const DATA = { LOAD:"LOAD_DATA", LOAD_SUCCESS:"LOAD_DATA_SUCCESS", LOAD_ERROR:"LOAD_DATA_ERROR" } export { DATA }
import { createStore,applyMiddleware} from 'redux' import rootReducer from '../reducers' import createSagaMiddleware from 'redux-saga' import rootSaga from '../sagas' const configureStore = () => { const sagaMiddleware = createSagaMiddleware() const store = createStore( rootReducer applyMiddleware(sagaMiddleware) ) sagaMiddleware.run(rootSaga) return store; } export default configureStore;
For reducers we basically have three states in this scenario :
1) Data loading
2) Data fetch success
3) Data fetch error
For all these three scenarios we will handle in three different reducer files, it can be handled in one file but this dividing way makes code more readable and maintainable
So let's create them one by one,
Data Loading reducer :
import { DATA } from '../constants' const loadingReducer = (state = false,action) => { switch(action.type){ case DATA.LOAD: return true; case DATA.LOAD_SUCCESS: case DATA.LOAD_ERROR: return false; default: return state; } } export default loadingReducer;
import { DATA } from '../constants' const dataReducer = (state = [],action) => { if(action.type === DATA.LOAD_SUCCESS){ return [...state,...action.data.data.data] }if(action.type === DATA.LOAD){ return [] }else{ return state } } export default dataReducer;
import { DATA } from '../constants' const errorReducer = (state = null,action) =>{ switch (action.type) { case DATA.LOAD_ERROR: return action.error case DATA.LOAD: case DATA.LOAD_SUCCESS: return null default: return state } } export default errorReducer;
Root Reducer:
import { combineReducers } from 'redux' import errorReducer from './errorReducer' import loadReducer from './loadReducer' import dataReducer from './dataReducer' const rootReducer = combineReducers({ error : errorReducer, isLoading :loadReducer, data : dataReducer }) export default rootReducer;
Data worker saga:
import { put,call } from 'redux-saga/effects' import { fetchRequest } from '../../api' import { setData,setError } from '../../actions'; export function* workerSaga(){ try{ const page = 1 const data = yield call(fetchRequest,page) yield put(setData(data)) }catch(err){ yield put(setError(err)) } }
import axios from 'axios' export const fetchRequest = async page => { const response = await axios.get(`https://reqres.in/api/users?page=${page}`) return response }
import { takeEvery } from 'redux-saga/effects' import { DATA } from '../constants' import { workerSaga as dataWorker } from './worker/dataWorkerSaga' function* rootSaga(){ console.log('watcher Saga') yield takeEvery(DATA.LOAD,dataWorker) } export default rootSaga;
import React,{useState,useEffect} from 'react'; import { connect } from 'react-redux'; import { loadData } from '../actions' import { DashboardStyle } from './style' const Gallery = (props) =>{ const [data,setData] = useState([]) useEffect(()=>{ setData(props.data) },[props.data]) const renderData = data.map((e,index) => ( <div key={index} style={DashboardStyle.tile}> <img alt={e.id} src={e.avatar} /> <div style={DashboardStyle.textContainer}> <span style={DashboardStyle.text}>First Name : {e.first_name} Last Name : {e.last_name} </span> <span style={DashboardStyle.text}>Email : {e.email} </span> <span style={DashboardStyle.text}>Id : {e.id}</span> </div> </div> )) return ( <> <button style={DashboardStyle.btn} onClick={()=>props.loadData()}>Get Data</button> <div> <span style={DashboardStyle.author}>https://reactcodes.blogspot.com</span> </div> {renderData} </> ) } const mapStateToProps = ({isLoading,data,error}) => ({ isLoading, data, error }) const mapDispatchToProps = dispatch => ({ loadData: ()=>dispatch(loadData()), }) export default connect(mapStateToProps,mapDispatchToProps)(Gallery);
Adding some styles :
export const DashboardStyle = { tile:{ display: 'flex', border: '3px solid #3594cb', padding: 10, width: '50%', margin: 10 }, text:{ fontSize:30 }, textContainer:{ display: 'grid', padding: '0 20px' }, btn:{ background: 'blue', color: 'white', width: 180, height: 50, fontSize: 36 }, author:{ fontSize: 36, color:'red' } }
Now in App.js file we need to pass store instance via provider
import React,{Fragment} from 'react'; import { Provider } from 'react-redux' import configureStore from './store' import Dashboard from './components/dashboard' const store = configureStore() const App = () => { return ( <Provider store={store}> <Fragment> <Dashboard /> </Fragment> </Provider> ); } export default App;
And we are done, we can test the application now :
And it is working :)
Well this solution is good when we have a single worker/watcher saga. But what if we have to handle multiple?
Handling multiple sagas
We need to modify rootSaga. And we well also place watcher saga in a different folder named watcher. Accordingly we need to update the worker also.
Let's assume we need a new action/saga for getting Country Capitals data, we will add corresponding actions in constant files and add reducers for he same. As it is same procedure I am not adding it here.
Let's focus on Sagas only.
Saga folder will look like below
dataWatcherSaga will be updated like below
import { DATA } from '../../constants' import {takeEvery} from 'redux-saga/effects' import {workerSaga as dataWorker} from '../workers/dataWorkerSaga' export default function* watchData(){ yield takeEvery(DATA.LOAD,dataWorker) }
Updated dataWorkerSaga
import { put,call } from 'redux-saga/effects' import { fetchRequest } from '../../api' import { setData,setError } from '../../actions'; export function* workerSaga(){ try{ const page = 1 const data = yield call(fetchRequest,page) yield put(setData(data)) }catch(err){ yield put(setError(err)) } }
Now we need to update rootSaga where we will add all the sagas
import { all } from 'redux-saga/effects'; import dataWatcher from './watchers/dataWatcherSaga' import countryWatcher from './watchers/countryCapitalsWatcherSaga' export default function* rootSaga() { yield all([ dataWatcher(), countryWatcher() ]); }
Now if you run the app it will handle multiple sagas.




Comments
Post a Comment