Customer: Use Case (4: Implementing Front-end Service)
Tags: javascript, react, vite, beginners
Published: 2023-11-30 02:15:43

## 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.
1. Open new `command prompt` from the `front-end-service` directory.
2. Create new React using Vite by running command below. I choose `React` as the framework and `JavaScript` as the variant.
    ``` 
    npm create vite@latest .
    ```
3. Run command:
    ```
    npm install
    ```
4. 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"
    },
    ```
4. 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
    ```

5. My `.env` file looks like below. Makesure that you have correct endpoints and use `VITE` 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. In`components` 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
1. Makesure you run the ExpressJS and NestJS service.
2. Back to `front-end-service` root directory, open command prompt directed to the directory.
2. Run `npm run dev`. You will see a localhost or similar address where you can see the interface.
3. If it is running correctly you will see this kind of user interface along with the functionality that we already build! 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3ynqfknz826fqkkemz2w.png)

4. 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:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/131cqgn3wcpwm78azsmo.png)
When I move to the NestJS tab I have this:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fza1xfkm9fbanv5bs8ue.png)
If you notice, the first one is hitting `http://localhost:3001` which is the ExpressJS service and the second one is hitting `http://localhost:3002` which is the NestJS service.