A MERN stack update for 2021. - Part B: Client side.

alanst32

alanst32

Posted on September 23, 2021

A MERN stack update for 2021. - Part B: Client side.

Hello there!
Since my last post my life have changed a lot, I've changed my job. Now I am a lead developer for Westpac Australia, and recently became a father. So it has been difficult to find times to come back here to finish my MERN 2021 article. But at last here we are.

To recap, the purpose of the article is to discuss a modern approach to a MERN stack, having a integration with Cloud solution (Azure) and use modern frameworks on the development like React, Typescript, RxJs and others.

On the first part I have described the server side solution of the MERN stack for our CRUD application. You can review it here: MERN Server-side

Now I will discuss the approach on the client side aspects like:

  • Webpack & Typescript ES6 configuration.
  • Client side NodeJS server
  • Observables with RxJS implementation
  • React modern implementation
  • Test cases

  • Requirments for this article:

  • React, NodeJS and Typescript basic knowledge.

MERN CLIENT-SIDE.

1 - Client Project.

The project consists on the UI development of the CRUD via configuration, UI and services implementation. Most of the project was developed by Typescript ES6 instead of standard Javascript. So for the bundle translation, it is used Webpack and Babel.

The CRUD app consists in a simple Students database, the user would be able to insert, delete a student, or to add new Skills.

Frameworks

  • React
  • Webpack 5
  • Babel
  • ExpressJS
  • Typescript
  • RxJS
  • Ts-node

Project structure

Alt Text

Node Server

On /server.ts is configured the NodeJs server of the project.

import express from 'express';
import path from 'path';
import bodyParser from 'body-parser';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import {Router} from 'express';
//==================================================================================

const app = express();
app.use(express.static(path.join('./')));
app.use(express.static('./src/components'));
app.use(express.static('./src/assets'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

const router = Router();
router.get('/', (req, res) => {
    console.log('got in the client router');
    res.render('index');
});
app.use('/', router);

// set engine for rendering
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, './src'));

const PORT = process.env.PORT || 4200;
//Express js listen method to run project on http://localhost:4200
app.listen(PORT, () => console.log(`App is running in ${process.env.NODE_ENV} mode on port ${PORT}`))
Enter fullscreen mode Exit fullscreen mode

/nodemon.json Here we configure nodemon, which is a tool that helps develop node.js based applications by automatically restarting the node application when file changes in the directory are detected..

{
    "watch": ["src", "dist"],
    "ext": "ts,js,tsx,jsx,ejs,scss,css",
    "exec":  "ts-node ./server.ts"
}
Enter fullscreen mode Exit fullscreen mode
Understanding the code.

One piece of MERN stack is [ExpressJS], (https://expressjs.com), a flexible Node.js web application framework that provides quick and easy APIs creation. It is through ExpressJs that the client project will access its Api's services. But before that, we need to configure Express in our server. On configuration above we set the static files directory and configure Express to expects requests that have "application/json" Content-Type headers and transform the text-based JSON input into JS-accessible variables under req.body.

Also I set Express to route path "/" to our home page. Then configure the server port to 4200.

As mentioned, I am using Typescript to set the server and rest of the components. Thus, we need to set the transformation of ES6 to CommonJs in the bundle file.

webpack.config.cjs

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpackNodeExternals = require('webpack-node-externals');
const isProduction = typeof NODE_ENV !== 'undefined' && NODE_ENV === 'production';
const devtool = isProduction ? false : 'inline-source-map';
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');

const clientConfig = {
    entry: './src/index.tsx',
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'bundle.js',
    },
    // plugins: [new HtmlWebpackPlugin()], 
    resolve: {
        extensions: ['.js', '.jsx', '.json', '.ts', '.tsx', '.ejs'],
        plugins: [new TsconfigPathsPlugin()],
    },
    devtool: 'inline-source-map', // Enable to debug typescript code
    module: {
        rules: [
            {
                test: /\.(jsx|js)$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            presets: ['@babel/preset-react', '@babel/preset-env'],
                        }
                    }
                ]
            },
            {
                test: /\.(tsx|ts)$/,
                use: 'ts-loader',
                exclude: /node_modules/
            },
            {
                test: /\.s[ac]ss$/i,
                use: [
                    // Creates `style` nodes from JS strings
                    'style-loader',
                    // Translates CSS into CommonJS
                    'css-loader',
                    // Compiles Sass to CSS
                    'sass-loader',
                ],
            },
            {
                test: /\.(png|jpg|gif|svg)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            outputPath: 'src/images/',
                            name: '[name][hash].[ext]',
                        },
                    },
                ],
            }
        ]
    }
};

module.exports = [clientConfig];
Enter fullscreen mode Exit fullscreen mode

/babel.config.json

{
    "presets": [
        ["@babel/env"],
        ["@babel/preset-react"]
    ],
    "plugins": ["react-hot-loader/babel"]
}
Enter fullscreen mode Exit fullscreen mode

As the last part of ES6 configuration, I set on ts.config file the bundle file location, the module to be used on the parsing and the most important Module resolution, which in our case is Node.
/tsconfig.json

{
    "compilerOptions": {
        "baseUrl": "./src",
        "outDir": "./dist/",
        "noImplicitAny": false,
        "module": "CommonJs",
        "target": "ESNext",
        "moduleResolution": "node",
        "jsx": "react",
        "allowJs": true,
        "strict": true,
        "allowSyntheticDefaultImports": true,
        "sourceMap": true,
        "esModuleInterop" : true,
        "typeRoots": [
            "./node_modules/@types"
        ],
        "lib": [
            "ESNext",
            "DOM"
        ], 
        "paths": {
            "@assets/*": ["assets/*"],
            "@app/*": ["components/*"],
            "@services/*": ["services/*"],
            "@models/*": ["models/*"]
        }
    },
    "include": [
        "./",
        "./src/assets/index.d.ts"
    ]
}
Enter fullscreen mode Exit fullscreen mode

App Init Configuration.

Now with server and ES6 parse configuration set we can finally move one with the development of our CRUD UI.

src/index.ejs

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta name="description" content="Alan Terriaga - MERN stack 2021 updated">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
    <!-- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> -->
    <title>MERN 2021</title>
    </head>
    <body>
        <div id="root"></div>
    </body>
    <script src="../dist/bundle.js"></script>
</html>
Enter fullscreen mode Exit fullscreen mode

src/index.tsx

import * as React from "react";
import * as ReactDOM from "react-dom";
import App from 'App';

ReactDOM.render(<App/>, document.getElementById('root'));
Enter fullscreen mode Exit fullscreen mode

src/App.tsx

import * as React from "react";
import Home from '@app/home';

export default function App() {
    return (
        <Home></Home>
    );
}
Enter fullscreen mode Exit fullscreen mode
Understanding the code.

No secrets so far, on index.ejs we import the common stylesheet libraries and the directory of the javascript bundle file generated by Webpack. Then, we link it to App and Home components as main entry to our CRUD application.

It is now that the app becomes interesting, but before describing our components I would like to show first the service class and how RxJS is used to publish API responses to new events.

Students Service Class.

src/services/student-service.ts

import { RestoreTwoTone } from "@material-ui/icons";
import StudentModel from "@models/student-model";
import axios, { AxiosResponse } from "axios";
import { Subject } from "rxjs";

export interface StudentRequest {
    name: string,
    skills: string[]
}

// AXIOS
const baseUrl = 'http://localhost:3000';
const headers = { 
    'Content-Type': 'application/json',
    mode: 'cors',
    credentials: 'include'
};

const axiosClient = axios;
axiosClient.defaults.baseURL = baseUrl;
axiosClient.defaults.headers = headers;

// GET Clients
const getStudentsSubject = new Subject<StudentModel[]>();
const getStudentsObservable = () => getStudentsSubject.asObservable();

const getStudents = async (body: StudentRequest) => {
    axiosClient.post<StudentModel[]>(
        '/student/list', 
        body
    )
    .then((res) => {
        console.log(`res.data: ${JSON.stringify(res.data)}`);
        res.data.forEach((res) => res.dateOfBirth = formatDate(res.dateOfBirth));
        getStudentsSubject.next(res.data);
    })
    .catch(ex => console.error(ex));
}

function formatDate(dob: string): string {
    const obj = new Date(dob);
    const aux = (obj.getMonth()+1);
    const month =  (aux < 10) ? `0${aux}` : aux;
    return `${obj.getDate()}/${month}/${obj.getFullYear()}`;
}

// INSERT STUDENT
const insertStudents = async (body: StudentModel) => {
    axiosClient.post(
        '/student',
        body
    )
    .catch(ex => console.error(ex));
}

const updateStudent = async (body: StudentModel) => {
    axiosClient.put(
        '/student',
        body
    )
    .catch(ex => console.error(ex));
}

const deleteStudents = async (ids: string[]) => {
    axiosClient.post(
        '/student/inactive',
        {ids}
    )
    .then((res) => {
        return;
    })
    .catch(ex => console.error(ex));
}

export {
    getStudents,
    getStudentsObservable,
    insertStudents,
    updateStudent,
    deleteStudents
}
Enter fullscreen mode Exit fullscreen mode
Understanding the code.

To request the APIS I have decided to use AXIOS as HTTP client, is a framework that has been around for a while and works really well, so I've seen no reason to change it on this matter. If you are not familiar with AXIOS please check its official website Axios-http.

Publishing events on RxJS.

According to the official website RxJS:

"RxJS is a library for composing asynchronous and event-based programs by using observable sequences. It provides one core type, the Observable, satellite types (Observer, Schedulers, Subjects) and operators inspired by Array#extras (map, filter, reduce, every, etc) to allow handling asynchronous events as collections."

In other words, is a framework that allows event asynchronously and any class that subscribe this event will listen the events when triggered. You might are familiar with event based frameworks, we have another examples like Kafka, Redux. I've seen the use of RxJS much more common on Angular application nowadays, although is works really fine in React apps as well.

To understand better how it works, let's get our attention to the GET post. First of all you would need to create a Subject class (Subject is similar to a EventEmitter class) is the only way to multicast messages/objects across the listeners.

With the help of Typescript and ES6 we can use Generics in our favour and map the Subject object as type of StudentModel interface.
Moving forward, you can see that after getting the response of Clients API I am publishing the response object into the Subject class. This will trigger a multicast for the active classes that are listening this event.

In order to reach that goal, you would notice also the Observable object created from the Subject. An Observable represents the idea of an invokable collection of future values or events, is through the Observable we will be able to lister the EventEmitter. Which is out next step.

Components and listening RxJS.

There is a lot to cover here, but to summarise the code we have our Home Component which is divided into StudentForm with the input fields and Insert Function. And StudentTable with the results of GET APIS.

src/components/home/index.tsx

import React, { useEffect, useState } from "react";
import StudentForm from '@app/home/student-form';
import UserTable from '@app/home/student-table';
import {getStudents, getStudentsObservable} from '@services/student-service';
import _ from 'lodash';
import StudentModel from "@models/student-model";
import StudentTable from "@app/home/student-table";
import { makeStyles, Theme, createStyles } from "@material-ui/core";

const useStyles = makeStyles((theme: Theme) =>
    createStyles({
        home: {
            width: '98%',
            justifyContent: 'center',
            textAlign: 'center',
            margin: 'auto'
        }
    }),
);

export default function Home() {
    const classes = useStyles();
    const[totalStudents, setTotalStudents] = useState(0);
    const[name, setName] = useState('');
    const[skills, setSkills] = useState<string[]>([]);
    const[students, setStudents] = useState<StudentModel[]>([]);
    const emptyStudentModel: StudentModel = {
        _id: '',
        firstName: '',
        lastName: '',
        country: '',
        dateOfBirth: '',
        skills: []
    };

    useEffect(() => {
        const request = {
            name,
            skills
        }
        getStudents(request);
    }, []);

    useEffect(() => {
        const subscription = getStudentsObservable().subscribe((list: StudentModel[]) => {
            if (!_.isEmpty(list)) {
                const size: number = list.length;
                const aux: StudentModel[] = list;
                setTotalStudents(users => size);
                list.forEach(x => x.checked = false);
                setStudents(list);
            }
            else {
                setTotalStudents(students => 0);
                setStudents(students => []);
            } 
        });

        return subscription.unsubscribe;
    },[]);

    return (
        <div className={classes.home}>
            <StudentForm totalStudents={totalStudents}></StudentForm>   
            <StudentTable students={students}></StudentTable>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode
Understanding the code.

Two major factors to pay attention here. First, since I am using functional components instead of React.Component classes, I am using the new (not so new) approach of React Hooks to control props and states changes. Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class. React-Hooks.

I am using the hook UseState to create the state variables and the Hook UseEffect to call Get Students API.
When you use UseEffect hook you tell React that your component needs to do something after render, if you specify a prop in the array you tell React to execute UseEffect only after that prop is changed. However, since I am not specifying a prop, I am telling React to execute UseEffect at first time the component is rendered. One cool feature to highlight here is since we are using Typescript we can set Generic types to our UseState hooks

The second factor here is the use of UseState to listen RxJS event GetStudents from the Observable object. As explained above when the EventEmitter is triggered the Observable class will listen and receive the object specified, in our case the list of Students. Which after that we only need to update our state variables for the next components.

src/components/home/student-form/index.tsx

import { Button, TextField, createStyles, makeStyles, Theme } from "@material-ui/core";
import React, { useState } from "react";
import { Image, Jumbotron } from "react-bootstrap";
import ReactImage from '@assets/svg/react.svg';
import { insertStudents, getStudents } from '@services/student-service';
import StudentModel from "@models/student-model";

const useStyles = makeStyles((theme: Theme) =>
    createStyles({
        header: {
            display: 'inline-block',
            width: '100%',
            marginBottom: '20px',
        },
        jumbotron: {
            height: '300px',
            width: '100%',
            display: 'grid',
            justifyContent: 'center',
            margin: 'auto',
            backgroundColor: 'lightblue',
            marginBottom: '10px',
        },
        form: {
            display: 'flex',
            justifyContent: 'center'
        },
        infoBox: {
            display: 'flex',
            justifyContent: 'center',
            verticalAlign: 'center'
        },
        labelStyle: {
            fontSize: '32px',
            fontWeight: 'bold',
            verticalAlign: 'center'
        },
        insertBtn: {
            marginLeft: '20px'
        }
    }),
);

function JumbotronHeader(props) {
    const classes = useStyles();
    const { totalStudents } = props;
    return (
        <Jumbotron className={classes.jumbotron}>
            <Image src={ReactImage}/>
            <h1>Students skills list: {totalStudents}</h1>
        </Jumbotron>
    );
}

export default function StudentForm(props) {
    const classes = useStyles();
    const [firstName, setFirstName ] = useState('');
    const [lastName, setLastName] = useState('');
    const [country, setCountry] = useState('');
    const [dateOfBirth, setDateOfBirth] = useState('');

    async function insertStudentAsync() {
        const request: StudentModel = {
            firstName,
            lastName,
            country,
            dateOfBirth,
            skills: [] 
        };
        await insertStudents(request);
        await getStudents({
            name: '',
            skills: []
        });
    }

    const { totalStudents } = props;

    return (
        <div className={classes.header}>
            <JumbotronHeader totalStudents={totalStudents}/>
            <form 
                className={classes.form}
                noValidate 
                autoComplete="off">
                <TextField 
                    id="firstName" 
                    label="First Name" 
                    variant="outlined" 
                    onChange={e => setFirstName(e.target.value)}/>
                <TextField 
                    id="lastName" 
                    label="Last Name" 
                    variant="outlined"
                    onChange={e => setLastName(e.target.value)}/>
                <TextField 
                    id="country" 
                    label="Country" 
                    variant="outlined"
                    onChange={e => setCountry(e.target.value)}/>
                <TextField 
                    id="dateOfBirth" 
                    label="DOB"
                    type="date" 
                    variant="outlined"
                    InputLabelProps={{
                        shrink: true,
                    }}
                    onChange={e => setDateOfBirth(e.target.value)}/>
                <Button 
                    id="insertBtn"
                    className={classes.insertBtn}
                    variant="contained" 
                    color="primary"
                    onClick={() => insertStudentAsync()}>
                    Insert
                </Button>
            </form>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

src/components/student-table/index.tsx

import { 
    Box, 
    Collapse, 
    IconButton, 
    Table, 
    TableCell, 
    TableHead, 
    TableBody, 
    TableRow, 
    Typography, 
    TableContainer,
    Checkbox,
    Button,
    createStyles,
    Dialog,
    DialogActions,
    DialogContent,
    DialogTitle,
    makeStyles,
    TextField,
    Theme
} from "@material-ui/core";
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
import Paper from '@material-ui/core/Paper';
import React, { useEffect, useState } from "react";
import StudentModel from "@models/student-model";
import { isEmpty } from 'lodash';
import { 
    getStudents, 
    updateStudent,
    deleteStudents
} from '@services/student-service';

const useStyles = makeStyles((theme: Theme) =>
    createStyles({
        userTable: {
            width: "100%",
            marginTop: "20px"
        },
        innerTable: {
            padding: "0px !important"
        },
        innerBox: {
            padding: "16px"
        },
        innerTableNoBottom: {
            padding: "0px !important",
            borderBottom: "0px !important"
        },
        skillsDialog: {
            width: "600%"
        },
        dialog: {
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
        },
        input: {
            width: "300px"
        },
        paper: {
            minWidth: "600px",
            backgroundColor: theme.palette.background.paper,
            border: '2px solid #000',
            boxShadow: theme.shadows[5],
            padding: theme.spacing(2, 4, 3),
        },
    }),
);

function getSkillsSummary(skills: string[]) {
    const summary: string = new Array(skills).join(",");
    return summary.length > 6 ? 
        `${summary.substring(0, 6)}...` :
        summary;
}

function SkillsDialog(props: {
    openDialog: boolean,
    handleSave,
    handleClose,
}) {
    const {
        openDialog,
        handleSave,
        handleClose
    } = props;
    const classes = useStyles();
    const [open, setOpen] = useState(false);
    const [inputText, setInputText] = useState('');

    useEffect(() => {
        setOpen(openDialog)
    }, [props]);

    return (
       <Dialog
            classes={{ paper: classes.paper}}
            open={open}
            onClose={handleClose}
            aria-labelledby="form-dialog-title">
                <DialogTitle id="form-dialog-title">Add a skill</DialogTitle>
                <TextField
                    autoFocus
                    className={classes.input}
                    margin="dense"
                    id="name"
                    onChange={e => setInputText(e.target.value)}
                />
                <DialogActions>
                    <Button 
                        color="primary"
                        onClick={() => handleClose()}>
                        Cancel
                    </Button>
                    <Button 
                        color="primary"
                        onClick={() => handleSave(inputText)}>
                        OK
                    </Button>
                </DialogActions>
        </Dialog>
    )
}

function Row(
    props: {
        student: StudentModel,
        handleCheck
    }
) {
    const classes = useStyles();
    const {student, handleCheck} = props;
    const [open, setOpen] = useState(false);
    const [openDialog, setOpenDialog] = useState(false);

    const openSkillsDialog = () => {
        setOpenDialog(true);
    }

    const closeSkillsDialog = () => {
        setOpenDialog(false);
    }

    async function saveSkillsAsync(newSkill: string) {
        const skills = student.skills;
        skills.push(newSkill);

        const request: StudentModel = {
            _id: student._id,
            firstName: student.firstName,
            lastName: student.lastName,
            country: student.country,
            dateOfBirth: student.dateOfBirth,
            skills: skills 
        };
        await updateStudent(request);
        await getStudents({
            name: '',
            skills: []
        });
        closeSkillsDialog();
    }

    return (
        <React.Fragment>
            <TableRow 
                className={classes.userTable}
                tabIndex={-1}
                key={student._id}
                role="checkbox">
                <TableCell padding="checkbox">
                    <Checkbox 
                        id={student._id}
                        onChange={(event) => handleCheck(event, student._id)}
                        checked={student.checked} 
                        inputProps={{'aria-labelledby': student._id}}/>
                </TableCell>
                <TableCell>
                    <IconButton 
                        aria-label="expand row"
                        size="small"
                        onClick={() => setOpen(!open)}>
                        {open ? <KeyboardArrowUpIcon/> : <KeyboardArrowDownIcon/>}
                    </IconButton>
                </TableCell>
                <TableCell scope="student">
                    {`${student.firstName} ${student.lastName}`} 
                </TableCell>
                <TableCell>
                    {student.dateOfBirth}
                </TableCell>
                <TableCell>
                    {student.country}
                </TableCell>
                <TableCell>
                    {getSkillsSummary(student.skills)}
                </TableCell>
            </TableRow>
            <TableRow>
                <TableCell 
                    className={open ? classes.innerTable: classes.innerTableNoBottom }
                    colSpan={6}>
                    <Collapse in={open}
                        timeout="auto"
                        unmountOnExit>
                        <Box className={classes.innerBox}>
                            <Typography 
                                variant="h5"
                                gutterBottom
                                component="div">
                                Skills
                            </Typography>
                            <Table size="small"
                                aria-label="skills">
                                <TableBody>
                                <Button 
                                    variant="contained" 
                                    color="primary"
                                    onClick={() => openSkillsDialog()}>
                                    Add Skill
                                </Button>
                                    {student.skills.map((skill) => (
                                         <TableRow key={skill}>
                                             <TableCell 
                                                component="th" 
                                                scope="skill">
                                                 {skill}
                                             </TableCell>
                                         </TableRow>
                                    ))}
                                    <SkillsDialog 
                                        openDialog={openDialog} 
                                        handleClose={closeSkillsDialog}
                                        handleSave={saveSkillsAsync}
                                    />
                                </TableBody>
                            </Table>
                        </Box>
                    </Collapse>
                </TableCell>
            </TableRow>
        </React.Fragment>    
    );
}

export default function StudentTable(props: {students: StudentModel[]}) {
    const [selectedAll, setSelectedAll] = useState(false);
    const [studentList, setStudentList] = useState<StudentModel[]>([]);

    useEffect(() => {
        setStudentList(props.students);
    }, [props]);

    const handleCheck = (event, id) => {
        const auxList = studentList;
        setStudentList((prevList) => {
            const aux = prevList.map(s => {
                const check = (s._id === id) ? event.target.checked : 
                    s.checked;
                return {
                    _id: s._id,
                    firstName: s.firstName,
                    lastName: s.lastName,
                    dateOfBirth: s.dateOfBirth,
                    country: s.country,
                    skills: s.skills,
                    checked: check
                }
            });
            return aux;
        });
    }

    const handleSelectAll = (event) => {
        const check = event.target.checked;
        setSelectedAll(check);
        setStudentList((prevList) => {
            const aux = prevList.map(s => {
                return {
                    _id: s._id,
                    firstName: s.firstName,
                    lastName: s.lastName,
                    dateOfBirth: s.dateOfBirth,
                    country: s.country,
                    skills: s.skills,
                    checked: check
                }
            });
            return aux;
        });
    }

    useEffect(()=> {
        if(!isEmpty(studentList)) {
            const filter = studentList.filter(s => !s.checked);
            setSelectedAll((prevChecked) => isEmpty(filter));
        }
    }, [studentList]);

    async function deleteStudentsAsync() {
        const filter: string[] = studentList
            .filter(s => s.checked === true)
            .map(x => x._id || '');
        if (!isEmpty(filter)) {
            await deleteStudents(filter);
            await getStudents({
                name: '',
                skills: []
            });
        }
    }

    return (
        <TableContainer component={Paper}>
            <Table aria-label="collapsible table">
                <TableHead>
                    <TableRow>
                        <TableCell>
                        <Checkbox
                            value={selectedAll}
                            checked={selectedAll}
                            onChange={(event) => handleSelectAll(event)}
                            inputProps={{ 'aria-label': 'Select all students' }} />
                        </TableCell>
                        <TableCell>
                        <Button 
                            variant="contained" 
                            color="primary"
                            onClick={() => deleteStudentsAsync()}>
                            Delete
                        </Button>
                        </TableCell>
                        <TableCell>Name</TableCell>
                        <TableCell>DOB</TableCell>
                        <TableCell>Country</TableCell>
                        <TableCell>Skills</TableCell>
                    </TableRow>
                </TableHead>
                <TableBody>
                    {studentList.map((row) => {
                        return (
                            <Row 
                                key={row._id} 
                                student={row} 
                                handleCheck={handleCheck} />
                        );
                    })}
                </TableBody>
            </Table>
        </TableContainer>
    );
}
Enter fullscreen mode Exit fullscreen mode
Understanding the code.

For the rest of the components, there is nothing here to explain that we didn't covered on the Home component above. The only exception is when we insert a new Student, the Get method is called straight after which eventually will generate a new event and trigger GetStudents observable to update the list.

I hope I could be clear with this enormous post, and if you have stayed with me until this end, thank you very much.
Don't forget to check it out the project on Github: mern-azure-client
Please feel free to comments for suggestions or tips.
See ya.
Alan Terriaga.

💖 💪 🙅 🚩
alanst32
alanst32

Posted on September 23, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related