Prerequisites
Installed Node version of v20.10.0
Brief Explanation
In this user interface I will fetch data from two services that I've created before which are ExpressJS service and NestJs service. For forms validation needs, I use Formik and Yup to make it easier. Lastly, since I just create a single page application I thought I would be overkill to use Redux, hence I use Lifting State Up
approach from child to its parent.
Initialize React-Vite application
I'm assuming that you have already created project directories just like what I did in the first post.
- Open new
command prompt
from thefront-end-service
directory. -
Create new React using Vite by running command below. I choose
React
as the framework andJavaScript
as the variant.
npm create vite@latest .
-
Run command:
npm install
-
Install these dependencies:
Material UI Dependencies
npm npm install @mui/material @emotion/react @emotion/styled
Material UI Icon Dependencies
npm install @mui/icons-material
Axios dependency
npm install axios
Formik Yup dependency
npm install formik yup
My
package.json
dependencies looks like:
"dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.14.18", "@mui/material": "^5.14.18", "axios": "^1.6.2", "formik": "^2.4.5", "react": "^18.2.0", "react-dom": "^18.2.0", "yup": "^1.3.2" },
-
Create project directory skeleton that looks like this:
front-end-service/ ├── src/ │ ├── assets/ │ ├── components/ │ ├── constants/ │ ├── services/ │ ├── utils/ │ ├── views/ │ ├── App.jsx │ ├── index.css │ ├── main.jsx │ └── theme.js ├── .env └── package.json
-
My
.env
file looks like below. Makesure that you have correct endpoints and useVITE
as the prefix.
VITE_BASE_URL_EXPRESS=http://localhost:3001 VITE_BASE_URL_NEST=http://localhost:3002
Configuring global theme
Since we are using Material UI, we have the independency to customize some components. I did few customization for typography, palette, and some components inside theme.js
as you can see below:
import { createTheme, responsiveFontSizes } from "@mui/material";
const firstLetterUppercase = {
'::first-letter': {
textTransform: 'uppercase',
}
}
let theme = createTheme({
palette: {
primary: {
main:'#FF4669',
contrastText: '#fff'
},
secondary: {
main: '#F5F5F5',
contrastText: '#BDBDBD'
}
},
typography: {
fontFamily: [
'Poppins',
'sans-serif'
].join(',')
},
components: {
MuiButton: {
styleOverrides: {
root: {
textTransform: 'none',
borderRadius: 20
}
}
},
MuiInputLabel: {
styleOverrides: {
root: firstLetterUppercase
}
},
MuiFormHelperText: {
styleOverrides: {
root: firstLetterUppercase
}
}
}
});
theme = responsiveFontSizes(theme);
export default theme;
Since I use Google Font, I need to define it inside index.css
. My final index.css
is look like this:
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap');
:root {
font-family: 'Poppins', sans-serif;
}
body {
background-color: #F5F5F5;
}
/* width */
::-webkit-scrollbar {
width: 10px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: #FF4669;
border-radius: 10px;
}
Now, to use our costumized theme, we have to define ThemeProvider
in App.jsx
. My final App.jsx
looks like this:
import { ThemeProvider } from '@emotion/react'
import theme from './theme'
import Dashboard from './views/Dashboard'
function App() {
return (
<ThemeProvider theme={theme}>
<Dashboard />
</ThemeProvider>
)
}
export default App
Creating constants
As I've said, I'll be fetching data from two services, to make it more consistent, I will create a file called constants.js
inside constants
directory with content like this:
export const Constant = {
EXPRESS_ID: 0,
NEST_ID: 1
};
Creating services and its helper function
Before we are creating some services to fetch data from APIs, I'll create a helper function to define the base url based on which service the data will be fetched. On utils
directory, create a file called helperFunction.js
and this is how it looks like:
import { Constant } from "../constants/constants"
export const defineBaseUrl = (serviceId) => {
const BASE_URL = serviceId === Constant.EXPRESS_ID ? import.meta.env.VITE_BASE_URL_EXPRESS : import.meta.env.VITE_BASE_URL_NEST;
return BASE_URL;
}
Now, we are going to create a file called index.js
inside services
directory. As you might notice, we have same endpoints for both ExpressJS and NestJS service just different host. So here's how I creating my service so I could fetch the data from the wanted service:
import axios from "axios";
import { defineBaseUrl } from "../utils/helperFunction";
let BASE_URL = "";
let BASE_PREFIX = "/api/customers"
export const createCustomer = async (serviceId, requestBody) => {
BASE_URL = defineBaseUrl(serviceId);
try {
const res = await axios.post(BASE_URL + BASE_PREFIX, requestBody);
return res;
} catch (error) {
console.log(error);
}
};
export const getCustomers = async (serviceId) => {
BASE_URL = defineBaseUrl(serviceId);
try {
const res = await axios.get(BASE_URL + BASE_PREFIX);
return res;
} catch (error) {
console.log(error);
}
};
export const getCustomerById = async (serviceId, customerId) => {
BASE_URL = defineBaseUrl(serviceId);
try {
const res = await axios.get(BASE_URL + BASE_PREFIX + `/${customerId}`);
return res;
} catch (error) {
console.log(error);
}
}
export const updateCustomer = async (serviceId, customerId, bodyRequest) => {
BASE_URL = defineBaseUrl(serviceId);
try {
const res = await axios.put(BASE_URL + BASE_PREFIX + `/${customerId}`, bodyRequest);
return res;
} catch (error) {
console.log(error);
}
};
export const deleteCustomer = async (serviceId, customerId) => {
BASE_URL = defineBaseUrl(serviceId);
try {
const res = await axios.delete(BASE_URL + BASE_PREFIX + `/${customerId}`);
return res;
} catch (error) {
console.log(error);
}
};
Creating components
To make it more structured, I'm going to create some components first.
Table of costumers component
In this component, I'm using MUI's table components. Incomponents
directory, create a new directory called CustomerDataList
, then create a new file called index.jsx
. It's a little tricky to do some loops because I want the user interface to use English as the language, but the response from backend is in Bahasa hence I do some tricky workaround. But the final look is like this:
import { CloseOutlined, DeleteForever, Edit, SearchRounded } from '@mui/icons-material';
import { Grid, IconButton, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography } from '@mui/material';
import { useFormik } from 'formik';
import React, { useEffect, useState } from 'react'
import * as Yup from 'yup';
import FormCustomerModal from '../FormCustomerModal';
import { deleteCustomer, getCustomerById, getCustomers, updateCustomer } from '../../services';
const header = [
"ID",
"Membership Number",
"Name",
"Address",
"City",
"Actions"
];
const structure = [
"id",
"no",
"nama",
"alamat",
"kota",
"aksi"
];
const initialValues = {
id: '',
no: '',
nama: '',
alamat: '',
kota: ''
};
const validationSchema = Yup.object().shape({
id: Yup.string(),
no: Yup.string(),
nama: Yup.string().required(),
alamat: Yup.string().required(),
kota: Yup.string().required(),
});
const CustomerDataList = ({ serviceID, onRefresh, onUpdate, onDelete }) => {
const [dataTable, setDataTable] = useState([]);
const [isUpdating, setIsUpdating] = useState(false);
const [searchedId, setSearchedId] = useState('');
const [isSearching, setIsSearching] = useState(false);
const [isDataNotFound, setIsDataNotFound] = useState(false);
const updateCustomerFormik = useFormik({
initialValues: initialValues,
validationSchema: validationSchema,
enableReinitialize: true,
onSubmit: () => {
handleUpdateCustomer();
setIsUpdating(false);
}
});
const handleUpdateFormikValue = ( index ) => {
updateCustomerFormik.setValues(dataTable[index]);
setIsUpdating(true)
}
const handleCloseUpdateModal = () => {
setIsUpdating(false);
}
const handleGetAllCustomers = () => {
getCustomers(serviceID)
.then((res) => {
if (res.status === 200) {
setDataTable([...res.data]);
}
}).catch((err) => {
console.log(err);
});
}
const handleUpdateCustomer = () => {
const customerId = updateCustomerFormik.values.id;
const requestBody = {
nama: updateCustomerFormik.values.nama,
alamat: updateCustomerFormik.values.alamat,
kota: updateCustomerFormik.values.kota
};
updateCustomer(serviceID, customerId, requestBody)
.then(() => {
onUpdate();
}).catch((err) => {
console.log(err);
});
}
const handleDeleteCustomer = ( index ) => {
const customerId = dataTable[index].id;
deleteCustomer(serviceID, customerId)
.then(() => {
onDelete()
}).catch((err) => {
console.log(err);
});
}
const handleSearchCustomer = () => {
if(searchedId !== '') {
setIsSearching(true);
getCustomerById(serviceID, searchedId)
.then((res) => {
if (res.status === 200 && Object.keys(res.data).length > 0) {
setIsDataNotFound(false);
setDataTable([res.data]);
} else {
setIsDataNotFound(true);
}
}).catch((err) => {
console.log(err);
});
}
}
const handleCloseSearching= () => {
setIsSearching(false);
setSearchedId('');
handleGetAllCustomers();
}
useEffect(() => {
handleGetAllCustomers();
}, [serviceID, onRefresh, isUpdating, onDelete]);
useEffect(() => {
setSearchedId('');
}, [serviceID]);
const ActionButtonGroups = ({ index }) => {
return (
<Grid container>
<Grid item>
<IconButton
color='warning'
onClick={() => handleUpdateFormikValue(index)}
>
<Edit />
</IconButton>
</Grid>
<Grid item>
<IconButton
color='error'
onClick={() => handleDeleteCustomer(index)}
>
<DeleteForever />
</IconButton>
</Grid>
</Grid>
);
}
return (
<>
<TextField
fullWidth
variant='filled'
placeholder='Search by id'
value={searchedId}
onChange={(e) => setSearchedId(e.target.value)}
sx={{
backgroundColor: 'primary.main',
borderTopLeftRadius: '8px',
borderTopRightRadius: '8px',
}}
inputProps={{
sx: {
paddingY: '1rem',
color: 'primary.contrastText'
},
}}
InputProps={{
endAdornment: (
isSearching
? (
<IconButton
sx={{ color: 'primary.contrastText' }}
onClick={handleCloseSearching}
>
<CloseOutlined />
</IconButton>
) : (
<IconButton
sx={{ color: 'primary.contrastText' }}
onClick={handleSearchCustomer}
>
<SearchRounded />
</IconButton>
)
)
}}
/>
<TableContainer component={Paper} sx={{ maxHeight: 375, boxShadow: 'none'}}>
<Table sx={{ width: '100%' }} stickyHeader>
<TableHead>
<TableRow>
{
header.map((item, index) => {
return (
<TableCell
align="left"
key={index}
sx={{
color: '#6D6C6D',
borderBottom: '2px solid',
borderColor: 'primary.main'
}}
>
{item}
</TableCell>
)
})
}
</TableRow>
</TableHead>
<TableBody>
{
isSearching && isDataNotFound
? (
<TableRow>
<TableCell colSpan={header.length} align={"center"}>
<Typography>No Customer Found</Typography>
</TableCell>
</TableRow>
) : (
dataTable.map((row, index) => {
return (
<TableRow key={index}>
{
structure.map((column) => {
return (
<TableCell>
{
column === 'aksi'
? <ActionButtonGroups index={index} />
: <Typography>{row[column.toLowerCase()]}</Typography>
}
</TableCell>
)
})
}
</TableRow>
)
})
)
}
</TableBody>
</Table>
</TableContainer>
<FormCustomerModal
title={"Update Customer Data"}
open={isUpdating}
handleClose={handleCloseUpdateModal}
formikProps={updateCustomerFormik}
disabledField={[ "id", "no", "createdAt", "updatedAt" ]}
/>
</>
)
}
export default CustomerDataList;
Form dialogue component
As you might notice in previous component, I have a component called FormCustomerModal. This component will be used as a dialogue containing form to add a customer and update an existing customer. First thing first, in components
directory, create a new folder called FormCustomerModal
, then inside that directory, create a new file called index.jsx
. The final look of this file is just like below:
import { Button, Dialog, DialogContent, DialogTitle, Grid, TextField } from '@mui/material';
import React from 'react'
const FormCustomerModal = ({ title, open, handleClose, formikProps, disabledField=[] }) => {
return (
<React.Fragment>
<Dialog
open={open}
onClose={handleClose}
fullWidth
>
<DialogTitle sx={{ textAlign: 'center' }}>{ title }</DialogTitle>
<DialogContent>
<form onSubmit={formikProps.handleSubmit}>
<Grid
container
spacing={2}
flexDirection={'column'}
sx={{ padding: '0.5rem'}}
>
{
Object.keys(formikProps.values).map((item, index) => {
return (
<Grid item key={index}>
<TextField
disabled={ disabledField.includes(item) ? true : false }
id="outlined-basic"
variant="outlined"
name={item}
label={item}
value={formikProps.values[item]}
onChange={formikProps.handleChange}
onBlur={formikProps.handleBlur}
error={formikProps.touched[item] && formikProps.errors[item]}
helperText={formikProps.touched[item] && formikProps.errors[item] && `${item} cannot be empty`}
fullWidth
/>
</Grid>
)
})
}
<Grid item sx={{ alignSelf: 'center' }}>
<Button variant='contained' type='submit'> Save Customer </Button>
</Grid>
</Grid>
</form>
</DialogContent>
</Dialog>
</React.Fragment>
)
}
export default FormCustomerModal;
Creating view of Dashboard
Now we already have all of the components and service, we can safely create the view. In views
directory, create a folder called Dashboard
then create a new file called indes.jsx
. This is the content of our dashboard view:
import { Alert, Button, Container, Grid, Tab, Tabs, Typography } from '@mui/material'
import React, { useEffect, useState } from 'react'
import CustomerDataList from '../../components/CustomerDataList'
import { AddCircleOutlineRounded } from '@mui/icons-material'
import * as Yup from 'yup';
import { useFormik } from 'formik';
import FormCustomerModal from '../../components/FormCustomerModal';
import { createCustomer } from '../../services';
import { Constant } from '../../constants/constants';
const initialValues = {
nama: '',
alamat: '',
kota: ''
};
const validationSchema = Yup.object().shape({
nama: Yup.string().required(),
alamat: Yup.string().required(),
kota: Yup.string().required(),
})
const Dashboard = () => {
const [tabValue, setTabValue] = useState(0);
const [isAddingNewCustomer, setIsAddingNewCustomer] = useState(false);
const [refreshTable, setRefreshTable] = useState(true);
const [alert, setAlert] = useState({
open: false,
severity: '',
message: ''
});
const handleTabChange = (e, newValue) => {
setTabValue(newValue);
}
const newCustomerFormik = useFormik({
initialValues: initialValues,
validationSchema: validationSchema,
onSubmit: (values) => {
handleAddNewCustomer(values);
}
});
const handleCloseAddingNewCustomer = () => {
setIsAddingNewCustomer(false);
}
const handleAddNewCustomer = async (values) => {
createCustomer(tabValue, values)
.then(() => {
setRefreshTable(true);
setAlert({
open: true,
severity: 'success',
message: `Successfully creating customer using Service ${tabValue === Constant.EXPRESS_ID ? 'ExpressJs': 'NestJs'}`
})
}).catch((err) => {
setRefreshTable(false);
}).finally(() => {
setIsAddingNewCustomer(false);
});
}
const handleUpdateCustomer = () => {
setAlert({
open: true,
severity: 'success',
message: `Successfully updating customer using Service ${tabValue === Constant.EXPRESS_ID ? 'ExpressJs': 'NestJs'}`
})
}
const handleDeleteCustomer = () => {
setAlert({
open: true,
severity: 'success',
message: `Successfully deleting customer using Service ${tabValue === Constant.EXPRESS_ID ? 'ExpressJs': 'NestJs'}`
})
}
useEffect(() => {
if (alert.open) {
const timeOut = setTimeout(() => {
setAlert({
open: false,
severity: '',
message: ''
});
}, 3000)
return () => {
clearTimeout(timeOut)
}
}
}, [alert])
return (
<Container>
<Alert
severity={alert.severity}
sx={{ visibility: alert.open ? 'visible' : 'hidden' }}
>
{alert.message}
</Alert>
<Grid
container
spacing={4}
flexDirection={"column"}
alignItems={"center"}
justifyContent={"center"}
height={'95vh'}
>
<Grid item width={"100%"}>
<Typography variant={"h5"} textAlign={"center"}>Customer List</Typography>
</Grid>
<Grid item width={"100%"}>
<Grid container spacing={4} flexDirection={"column"} paddingTop={0}>
<Grid item width={"100%"}>
<Tabs value={tabValue} onChange={handleTabChange} centered>
<Tab label="ExpressJS" sx={{ flexGrow: 1}} />
<Tab label="NestJS" sx={{ flexGrow: 1}} />
</Tabs>
</Grid>
<Grid item>
<CustomerDataList
serviceID={tabValue}
onRefresh={() => setRefreshTable(refreshTable)}
onUpdate={() => handleUpdateCustomer()}
onDelete={() => handleDeleteCustomer()}
/>
</Grid>
<Grid item alignSelf={"flex-end"}>
<Button
variant='contained'
startIcon={<AddCircleOutlineRounded />}
onClick={() => setIsAddingNewCustomer(true)}
>
Add Customer
</Button>
</Grid>
</Grid>
</Grid>
</Grid>
<FormCustomerModal
title={'Add New Customer'}
open={isAddingNewCustomer}
handleClose={handleCloseAddingNewCustomer}
formikProps={newCustomerFormik}
/>
</Container>
)
}
export default Dashboard;
Running application
- Makesure you run the ExpressJS and NestJS service.
- Back to
front-end-service
root directory, open command prompt directed to the directory. - Run
npm run dev
. You will see a localhost or similar address where you can see the interface. If it is running correctly you will see this kind of user interface along with the functionality that we already build!
To makesure that we did hit both services, we can see it at
Network
section while inspecting element like when I'm at ExpressJS tab, I have this:
When I move to the NestJS tab I have this:
If you notice, the first one is hittinghttp://localhost:3001
which is the ExpressJS service and the second one is hittinghttp://localhost:3002
which is the NestJS service.
Top comments (0)