Hello there!
On my previous postMERN client-side I have talked about a MERN client application with React, Typescript and the use of RxJs as an observables solution to collect and subscribe api response data.
Then I got on my mind, "How about Redux? Is it still worth?"
As we knowRedux is a state manager container for JavaScript apps. It is a robust framework that allows you to have state control and information in all components/containers of your application. It works like a flow with a single store, it can be used in any environment like react, angular 1/2, vanilla etc.
And to support the use of Redux in React we also haveReact-Redux. A library that allow us to keep Redux solution up to date with React modern approaches. Through React Hooks from React-Redux we can access and control the store. Is without saying that without React-Redux I would not recommend the use of Redux in applications today.
On that thought I have decided to create a different MERN client-side solution with React and Typescript but not this this time with Redux and React-Redux.
And to make the application even more robust I am usingRedux-Saga, which is basically a Redux side effect manager. Saga enables approaches to take parallel executions, task concurrency, task cancellation and more. You can also control threads with normal Redux actions. Comparing with React-Thunk, Saga it may seems complex at first but is a powerful solution. (But that's a talk for another post right ;) )
EDIT: As you may know, there are modern approaches to implement Redux on your application today. I have decided to write/keep this post in the "old" way in order o not cover multiple topics in the same article, with that on mind we could easily discuss new Saga/Redux approach in another article since the base is covered.
Now, without stretching too far, let's code!
1 - Client Project.
As this application is a similar solution from my previous post, I won't focus on the Node, Typescript and Webpack configuration. But exclusively on the Redux state flow between the CRUD operations.
Project structure
2 - Redux Flow.
As we know for our Redux flow we need to set:
- Redux Actions
- Redux Reducer
- Redux Selector
- Redux Store
And to work with the asynchronous calls to back end I am going to use a middleware layer.
- Redux Saga layer
Actions
src/redux/actions/studentActions.ts
importStudentModel,{StudentRequest}from"@models/studentModel";// TYPESexportenumSTUDENT_ACTIONS{GET_STUDENTS_REQUEST='GET_STUDENTS_REQUEST',GET_STUDENTS_SUCCESS='GET_STUDENTS_SUCCESS',GET_STUDENTS_ERROR='GET_STUDENTS_ERROR',INSERT_STUDENT_REQUEST='INSERT_STUDENT_REQUEST',INSERT_STUDENT_SUCCESS='INSERT_STUDENT_SUCCESS',INSERT_STUDENT_ERROR='INSERT_STUDENT_ERROR',UPDATE_STUDENT_REQUEST='UPDATE_STUDENT_REQUEST',UPDATE_STUDENT_SUCCESS='UPDATE_STUDENT_SUCCESS',UPDATE_STUDENT_ERROR='UPDATE_STUDENT_ERROR',DELETE_STUDENT_REQUEST='DELETE_STUDENT_REQUEST',DELETE_STUDENT_SUCCESS='DELETE_STUDENT_SUCCESS',DELETE_STUDENT_ERROR='DELETE_STUDENT_ERROR',ADD_SKILLS_REQUEST='ADD_SKILLS_REQUEST',ADD_SKILLS_SUCCESS='ADD_SKILLS_SUCCESS',ADD_SKILLS_ERROR='ADD_SKILLS_ERROR',};interfaceLoadingState{isLoading:boolean,}interfaceCommonErrorPayload{error?:{message:string,type:string,},}// ACTION RETURN TYPESexportinterfaceGetStudentsRequest{type:typeofSTUDENT_ACTIONS.GET_STUDENTS_REQUEST;args:StudentRequest,};exportinterfaceGetStudentsSuccess{type:typeofSTUDENT_ACTIONS.GET_STUDENTS_SUCCESS;payload:StudentModel[],};exportinterfaceGetStudentsError{type:typeofSTUDENT_ACTIONS.GET_STUDENTS_ERROR;payload:CommonErrorPayload,};exportinterfaceInsertStudentRequest{type:typeofSTUDENT_ACTIONS.INSERT_STUDENT_REQUEST;args:StudentModel,}exportinterfaceInsertStudentSuccess{type:typeofSTUDENT_ACTIONS.INSERT_STUDENT_SUCCESS,};exportinterfaceInsertStudentError{type:typeofSTUDENT_ACTIONS.INSERT_STUDENT_ERROR;payload:CommonErrorPayload,};exportinterfaceUpdateStudentRequest{type:typeofSTUDENT_ACTIONS.UPDATE_STUDENT_REQUEST;args:StudentModel,};exportinterfaceUpdateStudentSuccess{type:typeofSTUDENT_ACTIONS.UPDATE_STUDENT_SUCCESS,};exportinterfaceUpdateStudentError{type:typeofSTUDENT_ACTIONS.UPDATE_STUDENT_ERROR;payload:CommonErrorPayload,};exportinterfaceDeleteStudentRequest{type:typeofSTUDENT_ACTIONS.DELETE_STUDENT_REQUEST;args:string[],};exportinterfaceDeleteStudentSuccess{type:typeofSTUDENT_ACTIONS.DELETE_STUDENT_SUCCESS,};exportinterfaceDeleteStudentError{type:typeofSTUDENT_ACTIONS.DELETE_STUDENT_ERROR;payload:CommonErrorPayload,};// ACTIONSexportconstgetStudentsRequest=(args:StudentRequest):GetStudentsRequest=>({type:STUDENT_ACTIONS.GET_STUDENTS_REQUEST,args,});exportconstgetStudentsSuccess=(payload:StudentModel[]):GetStudentsSuccess=>({type:STUDENT_ACTIONS.GET_STUDENTS_SUCCESS,payload,});exportconstgetStudentsError=(payload:CommonErrorPayload):GetStudentsError=>({type:STUDENT_ACTIONS.GET_STUDENTS_ERROR,payload,});exportconstinsertStudentRequest=(args:StudentModel):InsertStudentRequest=>({type:STUDENT_ACTIONS.INSERT_STUDENT_REQUEST,args,});exportconstinsertStudentSuccess=():InsertStudentSuccess=>({type:STUDENT_ACTIONS.INSERT_STUDENT_SUCCESS,});exportconstinsertStudentError=(payload:CommonErrorPayload):InsertStudentError=>({type:STUDENT_ACTIONS.INSERT_STUDENT_ERROR,payload,});exportconstupdateStudentRequest=(args:StudentModel):UpdateStudentRequest=>({type:STUDENT_ACTIONS.UPDATE_STUDENT_REQUEST,args,});exportconstupdateStudentSuccess=():UpdateStudentSuccess=>({type:STUDENT_ACTIONS.UPDATE_STUDENT_SUCCESS,});exportconstupdateStudentError=(payload:CommonErrorPayload):UpdateStudentError=>({type:STUDENT_ACTIONS.UPDATE_STUDENT_ERROR,payload,});exportconstdeleteStudentRequest=(args:string[]):DeleteStudentRequest=>({type:STUDENT_ACTIONS.DELETE_STUDENT_REQUEST,args,});exportconstdeleteStudentSuccess=():DeleteStudentSuccess=>({type:STUDENT_ACTIONS.DELETE_STUDENT_SUCCESS,});exportconstdeleteStudentError=(payload:CommonErrorPayload):DeleteStudentError=>({type:STUDENT_ACTIONS.DELETE_STUDENT_ERROR,payload,});
Understanding the code.
No mystery here. On a redux flow we need to set which actions will be part of the state control, and for each CRUD operation I have set a state of REQUEST, SUCCESS and ERROR result. Which you will understand the reason why following below.
One interesting point here is since I am coding in Typescript I can benefit of Enum and Types usage to make our code clearer and more organised.
Reducer
src/redux/reducer/studentReducer.ts
import{STUDENT_ACTIONS}from"redux/actions/studentActions";constinitialState={isGetStudentsLoading:false,data:[],getStudentsError:null,isInsertStudentLoading:false,insertStudentError:null,isUdpateStudentLoading:false,updateStudentError:null,isDeleteStudentLoading:false,deleteStudentError:null,};exportdefault(state=initialState,action)=>{switch(action.type){caseSTUDENT_ACTIONS.GET_STUDENTS_REQUEST:return{...state,isGetStudentsLoading:true,getStudentsError:null,};caseSTUDENT_ACTIONS.GET_STUDENTS_SUCCESS:return{...state,isGetStudentsLoading:false,data:action.payload,getStudentsError:null,};caseSTUDENT_ACTIONS.GET_STUDENTS_ERROR:return{...state,isGetStudentsLoading:false,data:[],getStudentsError:action.payload.error,};// INSERTcaseSTUDENT_ACTIONS.INSERT_STUDENT_REQUEST:return{...state,isInsertStudentLoading:true,insertStudentError:null,};caseSTUDENT_ACTIONS.INSERT_STUDENT_ERROR:return{...state,isInsertStudentLoading:false,insertStudentError:action.payload.error,};// UPDATEcaseSTUDENT_ACTIONS.UPDATE_STUDENT_REQUEST:return{...state,isUdpateStudentLoading:true,updateStudentError:null,};caseSTUDENT_ACTIONS.UPDATE_STUDENT_ERROR:return{...state,isUdpateStudentLoading:false,updateStudentError:action.payload.error,};// DELETEcaseSTUDENT_ACTIONS.DELETE_STUDENT_REQUEST:return{...state,isDeleteStudentLoading:true,deleteStudentError:null,};caseSTUDENT_ACTIONS.DELETE_STUDENT_ERROR:return{...state,isDeleteStudentLoading:false,deleteStudentError:action.payload.error,};default:return{...initialState,}}}
src/redux/reducer/rootReducer.ts
import{combineReducers}from"redux";importstudentReducerfrom"./studentReducer";constrootReducer=combineReducers({entities:combineReducers({student:studentReducer,}),});exporttypeAppState=ReturnType<typeofrootReducer>;exportdefaultrootReducer;
Understanding the code.
Reducers are functions that takes the current state and an action as argument, and return a new state result. In other words, (state, action) => newState.
And in the code above I am setting how the Student state model is going to be according to each action received. As you can see the whole state is not being overwritten, but just the necessary attributes according to the action.
This application only has one reducer, but in most of the cases you will break down your reducers in different classes. To wrap them together we have therootReducer class. Which basically combines all the reducers in the state.
Selector
In simple words, a "selector" is a function that accepts the state as an argument and returns a piece of data that you desire from the store.
But of course it has more finesse than that, it is an efficient way to keep the store at minimal and is not computed unless one of its arguments changes.
src/redux/selector/studentSelector.ts
import{get}from'lodash';import{createSelector}from'reselect';import{AppState}from'@redux/reducer/rootReducer';constentity='entities.student';constgetStudentsLoadingState=(state:AppState)=>get(state,`${entity}.isGetStudentsLoading`,false);constgetStudentsState=(state:AppState)=>get(state,`${entity}.data`,[]);constgetStudentsErrorState=(state:AppState)=>get(state,`${entity}.getStudentsError`);exportconstisGetStudentsLoading=createSelector(getStudentsLoadingState,(isLoading)=>isLoading);exportconstgetStudents=createSelector(getStudentsState,(students)=>students);exportconstgetStudentsError=createSelector(getStudentsErrorState,(error)=>error);constinsertStudentLoadingState=(state:AppState)=>get(state,`${entity}.isInsertStudentLoading`,false);constinsertStudentErrorState=(state:AppState)=>get(state,`${entity}.insertStudentError`);exportconstisInsertStudentLoading=createSelector(insertStudentLoadingState,(isLoading)=>isLoading);exportconstinsertStudentError=createSelector(insertStudentErrorState,(error)=>error);constupdateStudentLoadingState=(state:AppState)=>get(state,`${entity}.isUdpateStudentLoading`,false);constupdateStudentErrorState=(state:AppState)=>get(state,`${entity}.updateStudentError`);exportconstisUpdateStudentLoading=createSelector(updateStudentLoadingState,(isLoading)=>isLoading);exportconstupdateStudentError=createSelector(updateStudentErrorState,(error)=>error);constdeleteStudentLoadingState=(state:AppState)=>get(state,`${entity}.isDeleteStudentLoading`,false);constdeleteStudentErrorState=(state:AppState)=>get(state,`${entity}.deleteStudentError`);exportconstisDeleteStudentLoading=createSelector(deleteStudentLoadingState,(isLoading)=>isLoading);exportconstdeleteStudentError=createSelector(deleteStudentErrorState,(error)=>error);constisAddSkillsLoadingState=(state:AppState)=>get(state,`${entity}.isAddSkillsLoading`,false);constaddSkillErrorState=(state:AppState)=>get(state,`${entity}.addSkillsError`);exportconstisAddSkillsLoading=createSelector(isAddSkillsLoadingState,(isLoading)=>isLoading);exportconstaddSkillsError=createSelector(addSkillErrorState,(error)=>error);
Understanding the code.
With the selector concept on mind, we can take from the code above is that we are returning the desire part of the store we need according to the function created.
For instance ingetStudentsLoadingState I don't need to return the whole store to the caller, but only the flag that indicates whether the students are being loaded instead.
Store
The Redux store brings together the state, actions and reducers to the application. Is an immutable object tree that holds the current application state. Is through the store we will access the state info and dispatch actions to update its state information. Redux can have only a single store in your application.
src/redux/store/store.ts
import{createStore,applyMiddleware}from'redux';importcreateSagaMiddlewarefrom'@redux-saga/core';import{composeWithDevTools}from'redux-devtools-extension';importrootReducerfrom'../reducer/rootReducer';importloggerfrom'redux-logger';import{rootSaga}from'@redux/saga/rootSaga';constinitialState={};constsagaMiddleware=createSagaMiddleware();conststore=createStore(rootReducer,initialState,composeWithDevTools(applyMiddleware(sagaMiddleware,logger)));sagaMiddleware.run(rootSaga)exportdefaultstore;
Understanding the code.
For Store creation, it is required to set the Reducer or the Reducers combined and the initial state of the application.
And if you are using a middleware like I am, the middleware is also required to be set into the store. In this case is the classrootSaga which I am describing below.
Saga
According to Saga website:
In redux-saga, Sagas are implemented using Generator functions. To express the Saga logic, we yield plain JavaScript Objects from the Generator. We call those Objects Effects. ...You can view Effects like instructions to the middleware to perform some operation (e.g., invoke some asynchronous function, dispatch an action to the store, etc.).
With Saga we can instruct the middleware to fetch or dispatch data according to an action for example. But of course is more complex than that, but don't worry I will break down and explain the code below into pieces.
With Saga I can set the application to dispatch or fetch APIS according to the action received.
src/redux/saga/studentSaga.ts
import{all,call,put,takeLatest,takeLeading}from"redux-saga/effects";importStudentModel,{StudentRequest}from'@models/studentModel';import{formatDate}from'@utils/dateUtils';import{get}from'lodash';importaxiosfrom'axios';import{isEmpty}from'lodash';import{deleteStudentError,getStudentsError,getStudentsRequest,getStudentsSuccess,insertStudentError,STUDENT_ACTIONS,updateStudentError}from"@redux/actions/studentActions";// AXIOSconstbaseUrl='http://localhost:3000';constheaders={'Content-Type':'application/json',mode:'cors',credentials:'include'};constaxiosClient=axios;axiosClient.defaults.baseURL=baseUrl;axiosClient.defaults.headers=headers;constgetStudentsAsync=(body:StudentRequest)=>{returnaxiosClient.post<StudentModel[]>('/student/list',body);}function*getStudentsSaga(action){try{constargs=get(action,'args',{})constresponse=yieldcall(getStudentsAsync,args);yieldput(getStudentsSuccess(response.data));}catch(ex:any){consterror={type:ex.message,// something else can be configured heremessage:ex.message,};yieldput(getStudentsError({error}));}}constinsertStudentsAsync=async(body:StudentModel)=>{returnaxiosClient.post('/student',body)}function*insertStudentSaga(action){try{conststudentModel=get(action,'args');if(studentModel==null){thrownewError('Request is null');}yieldcall(insertStudentsAsync,studentModel);constgetAction={type:STUDENT_ACTIONS.GET_STUDENTS_REQUEST,args:{},};yieldcall(getStudentsSaga,getAction);}catch(ex:any){consterror={type:ex.message,// something else can be configured heremessage:ex.message,};yieldput(insertStudentError({error}));}};constupdateStudentAsync=async(body:StudentModel)=>{returnaxiosClient.put('/student',body);};/** * * @param action {type, payload: StudentModel} */function*updateStudentSaga(action){try{conststudentModel=get(action,'args');if(studentModel==null){thrownewError('Request is null');};yieldcall(updateStudentAsync,studentModel);constgetStudentRequestAction=getStudentsRequest({});yieldcall(getStudentsSaga,getStudentRequestAction);}catch(ex:any){consterror={type:ex.message,// something else can be configured heremessage:ex.message,};yieldput(updateStudentError({error}));}};constdeleteStudentsAsync=async(ids:string[])=>{returnaxiosClient.post('/student/inactive',{ids});};/** * * @param action {type, payload: string[]} */function*deleteStudentSaga(action){try{constids=get(action,'args');if(isEmpty(ids)){thrownewError('Request is null');};yieldcall(deleteStudentsAsync,ids);constgetStudentRequestAction=getStudentsRequest({});yieldcall(getStudentsSaga,getStudentRequestAction);}catch(ex:any){consterror={type:ex.message,// something else can be configured heremessage:ex.message,};yieldput(deleteStudentError({error}));}};function*studentSaga(){yieldall([takeLatest(STUDENT_ACTIONS.GET_STUDENTS_REQUEST,getStudentsSaga),takeLeading(STUDENT_ACTIONS.INSERT_STUDENT_REQUEST,insertStudentSaga),takeLeading(STUDENT_ACTIONS.UPDATE_STUDENT_REQUEST,updateStudentSaga),takeLeading(STUDENT_ACTIONS.DELETE_STUDENT_REQUEST,deleteStudentSaga),]);}exportdefaultstudentSaga;
Understanding the code.
Let's break into pieces here:
1 - Exported functionstudentSaga().
To put it simple, I am telling SAGA to wait for an action and then to perform or call a function. For instance whenGET_STUDENTS_REQUEST is dispatched by Redux, I am telling SAGA to callgetStudentsSaga method.
But in order to achieve that I have to use the SAGA API, in specific the methods:
- takeLatest: Forks a saga on each action dispatched to the store that matches the pattern. And automatically cancels any previous saga task started previously if it's still running. In other words, ifGET_STUDENTS_REQUEST is dispatched multiple times, SAGA will cancel the previous fetch and create a new one.
- takeLeading: The difference here is that after spawning a task once, it blocks until spawned saga completes and then starts to listen for a pattern again.
- yieldAll: Creates an Effect that instructs Saga to run multiple Effects in parallel and wait for all of them to complete. Here we set our actions to the attached Saga fork method to run in parallel in the application.
2 - Updating the Store with SAGA_.
Now that the (action/methods) are attached to Saga effects, we can proceed to the creation of effects in order to call APIS or update the Redux Store.
3 - getStudentsSaga()_ method.
More SAGA API is used here:
- yield call: Creates an Effect that calls the function attached with args as arguments. In this case, the function called is an Axios API POST that returns a Promise. And since is a Promise, Saga suspends the generator until the Promise is resolved with response value, if the Promise is rejected an error is thrown inside the Generator.
- yield put: Here, I am setting the store with the new Student list data, by creating an Effect that instructs Saga to schedule an action to the store. This dispatch may not be immediate since other tasks might lie ahead in the saga task queue or still be in progress. You can, however expects that the store will be updated with the new state value.
The rest of the class is more of the same flow, I operate the CRUD methods accordingly to the logic and use the same Saga effects necessary to do it.
But Saga offers way more possibilities, don't forget to check it out itsAPI reference for more options.
4 rootSaga.
By this time you might have been wondering, "Where is the rootSaga specified on the Store?".
Below we have therootSaga class, which follows the same principle asrootReducer. Here we combines all Saga classes created on the application.
src/redux/saga/rootSaga.ts
import{all,fork}from"redux-saga/effects";importstudentSagafrom"./studentSaga";exportfunction*rootSaga(){yieldall([fork(studentSaga)]);};
3 - Hook up Redux with React.
Now that all redux flow is set, is time to hoop up with React Components, to do that we just need to attach the Redux Store as a provider to the application.
src/index.tsx
import*asReactfrom"react";import*asReactDOMfrom"react-dom";importAppfrom'App';import{Provider}from'react-redux';importstorefrom"@redux/store/store";ReactDOM.render(<Providerstore={store}><App/></Provider>,document.getElementById('root'));
4 - Use of Redux on Components.
For last, we now are able to consume state and dispatch actions from/to Redux, at first we will dispatch an action to tell Redux and Saga to fetch students data.
Note: For the purpose of this article and to focus on Redux I have shortened the code in areas not related to Redux. However, if would be able to check the whole code, you can check tis Git Repository, the link is by the end of this post.
Fetching data.
src/components/home/index.tsx
importReact,{useEffect,useState}from"react";import_from'lodash';importStudentModel,{StudentRequest}from"@models/studentModel";importStudentFormfrom"@app/studentForm";importStudentTablefrom"@app/studentTable";import{useDispatch}from"react-redux";import{createStyles,makeStyles}from'@mui/styles';import{Theme}from'@mui/material';import{getStudentsRequest}from"@redux/actions/studentActions";constuseStyles=makeStyles((theme:Theme)=>createStyles({...}),);exportdefaultfunctionHome(){constclasses=useStyles();constdispatch=useDispatch();constemptyStudentModel:StudentModel={_id:'',firstName:'',lastName:'',country:'',dateOfBirth:'',skills:[]};useEffect(()=>{constargs:StudentRequest={name:'',skills:[],};dispatch(getStudentsRequest(args));},[]);return(<divclassName={classes.home}><StudentForm></StudentForm><StudentTable></StudentTable></div>);}
Understanding the code.
With the new updates on React and React-Redux framework we can now use specific hooks on functional components to manage our state with Redux.
On the code above through the hookuseEffect an action is dispatched to fetch Students data.
- useDispatch: This hooks replicates the oldmapDispatchToProps method, which is to set dispatch actions to the redux store. And since the code is in typescript, we can take the advantages of passing actions that are already mapped by interfaces. But underneath what is happening is the same as:
dispatch({type:'GET_STUDENTS_REQUEST',args:{name:'',skills:[]}})
Saving and reloading state data.
Now that the data is loaded, we can proceed with the rest of CRUD operations.
src/components/studentForm/index.tsx
import{Button,TextField,Theme}from'@mui/material';import{createStyles,makeStyles}from'@mui/styles';importReact,{useState}from"react";import{Image,Jumbotron}from"react-bootstrap";importlogofrom'@assets/svg/logo.svg';importStudentModelfrom"@models/studentModel";import{useSelector}from"react-redux";import{isEmpty}from'lodash';import{getStudents}from"@redux/selector/studentSelector";import{insertStudentRequest}from"@redux/actions/studentActions";import{useDispatch}from"react-redux";constuseStyles=makeStyles((theme:Theme)=>createStyles({{...}}),);functionJumbotronHeader(props){constclasses=useStyles();const{totalStudents}=props;return(<Jumbotron.../>);}exportdefaultfunctionStudentForm(props){conststudents=useSelector(getStudents);constdispatch=useDispatch();constclasses=useStyles();const[firstName,setFirstName]=useState('');const[lastName,setLastName]=useState('');const[country,setCountry]=useState('');const[dateOfBirth,setDateOfBirth]=useState('');consttotalStudents=isEmpty(students)?0:students.length;asyncfunctioninsertStudentAsync(){constrequest:StudentModel={firstName,lastName,country,dateOfBirth,skills:[]};dispatch(insertStudentRequest(request));}return(<divclassName={classes.header}><JumbotronHeadertotalStudents={students.length}/><form>// Form Components{...}<Buttonid="insertBtn"onClick={()=>insertStudentAsync()}>Insert</Button></form></div>);}
Highlights
What is important to here is when the button is clicked a Redux Action is dispatched byuseDispatch hook, to insert student data on database and also to refresh the student list afterwards.
src/components/studentTable/index.tsx
importReact,{useEffect,useState}from"react";importStudentModelfrom"@models/studentModel";import{isEmpty}from'lodash';import{getStudents,isGetStudentsLoading}from"@redux/selector/studentSelector";import{deleteStudentRequest,updateStudentRequest}from"@redux/actions/studentActions";import{useDispatch,useSelector}from"react-redux";import{shadows}from'@mui/system';import{createStyles,makeStyles}from'@mui/styles';import{...}from'@mui/material';import{KeyboardArrowDown,KeyboardArrowUp}from'@mui/icons-material'constuseStyles=makeStyles((theme:Theme)=>createStyles({{...}}),);functiongetSkillsSummary(skills:string[]){{...}}functionSkillsDialog(props:{openDialog:boolean,handleSave,handleClose,}){const{openDialog,handleSave,handleClose}=props;constclasses=useStyles();const[open,setOpen]=useState(false);const[inputText,setInputText]=useState('');useEffect(()=>{setOpen(openDialog)},[props]);return(<Dialogopen={open}onClose={handleClose}>{...}</Dialog>)}functionRow(props:{student:StudentModel,handleCheck}){constclasses=useStyles();constdispatch=useDispatch();const{student,handleCheck}=props;const[open,setOpen]=useState(false);const[openDialog,setOpenDialog]=useState(false);constopenSkillsDialog=()=>{...};constcloseSkillsDialog=()=>{...};asyncfunctionsaveSkillsAsync(newSkill:string){constskills=student.skills;skills.push(newSkill);constrequest:StudentModel={_id:student._id,firstName:student.firstName,lastName:student.lastName,country:student.country,dateOfBirth:student.dateOfBirth,skills:skills};dispatch(updateStudentRequest(request));closeSkillsDialog();}return(<React.Fragment><TableRow...>{...}</TableRow><TableRow><TableCell...><Collapse...><BoxclassName={classes.innerBox}><Typography...><Table...><TableBody><Button...>{student.skills.map((skill)=>(<TableRowkey={skill}><TableCell...></TableRow>))}<SkillsDialogopenDialog={openDialog}handleClose={closeSkillsDialog}handleSave={saveSkillsAsync}/></TableBody></Table></Box></Collapse></TableCell></TableRow></React.Fragment>);}exportdefaultfunctionStudentTable(){constdispatch=useDispatch();conststudents:StudentModel[]=useSelector(getStudents);constisLoading:boolean=useSelector(isGetStudentsLoading);const[selectedAll,setSelectedAll]=useState(false);const[studentList,setStudentList]=useState<StudentModel[]>([]);useEffect(()=>{setStudentList(students);},[students]);useEffect(()=>{{...}},[studentList]);consthandleCheck=(event,id)=>{{...}}consthandleSelectAll=(event)=>{{...}}asyncfunctiondeleteStudentsAsync(){constfilter:string[]=studentList.filter(s=>s.checked===true).map(x=>x._id||'');if(!isEmpty(filter)){dispatch(deleteStudentRequest(filter));};}constLoadingCustom=()=>{...}return(<TableContainercomponent={Paper}>{isLoading&&(<LoadingCustom/>)}{!isLoading&&(<Tablearia-label="collapsible table"><TableHead><TableRow><TableCell><Checkbox.../></TableCell><TableCell><Buttonvariant="contained"color="primary"onClick={()=>deleteStudentsAsync()}>Delete</Button></TableCell><TableCell>{...}</TableCell></TableRow></TableHead><TableBody>{studentList.map((row)=>{return(<Row.../>);})}</TableBody></Table>)}</TableContainer>);}
Highlights
- useSelector: Similar to useDispatch this hook replicatesmapStateToProps redux old method. Allows you to extract data from the Redux store state, using a selector function. In our example I am loading students list data from store.
As for the rest of CRUD operations I continue to useuseDispatch to perform the actions necessary.
Final Considerations and GIT.
With the new behaviour of functional components creation in React. React-Redux hooks extends Redux lifetime. Otherwise I would not recommend using Redux instead of RxJS for example. Furthermore, using SAGA as middleware make the application even more robust, which allows us to control the effects of asynchronous calls through the system.
If you have stayed until the end, thank you very much. And please let me know your thoughts about the usage of Redux on current present.
You can check the whole code of the project on its git repository:MERN-CLIENT-REDUX.
See ya.
Top comments(7)

Please note that this is all based on a very old style of Redux itself that we are no longer teaching for production use.
We officially recommend to use the official Redux Toolkit for any Redux code written nowadays - in new projects as well as in old projects.
Working with Redux Toolkit, you won't have to deal with switch..case reducers, ACTION_TYPE string constants, immutable reducer logic and all that stuff. It essentially reduces your code to a fourth, works much better with TypeScript and includes RTK-Query, which is essentially "React Query for Redux".
I'd recommend going with the official Redux tutorial to get up to speed with the most modern approaches:redux.js.org/tutorials/essentials/...

- LocationMelbourne, Australia
- WorkAWS Cloud Native Tech Specialist at Cevo
- Joined
Thanks for the comment, I am aware of React Query, I am working in something right now maybe I will post it here. My main objective was to describe Saga, so I have done in the "old" way to point step by step. I thought the modern approach would be a material for another article ;)

Just to make sure: I am not talking about "React Query" here. I am talking about "RTK Query", which is part of the official Redux Toolkit.
And also when using Saga, the general official recommendation to use Redux Toolkit for that still stands (for two years now:redux.js.org/style-guide/style-gui...).
We are not teaching Vanilla Redux for any kind of production use any more and it would be great to see articles picking up on that.

- LocationMelbourne, Australia
- WorkAWS Cloud Native Tech Specialist at Cevo
- Joined
Good to know, I will take a look on the links. My projects lately have been with RxJs, so I guess I need to catch up with Redux modern approaches. But is like I always say Adapt and Evolve.
Cheers mate.

- LocationMelbourne, Australia
- WorkAWS Cloud Native Tech Specialist at Cevo
- Joined
Agree, solutions like React Query or RxJS would be much less code. The reason of the post was not to say that Redux would be a better option, but to demonstrate how we can achieve a state management with Typescript and Redux.
For further actions, you may consider blocking this person and/orreporting abuse