I have this Spring Boot endpoint for listing items from database:
import React, { useEffect, useState } from "react";
import clsx from "clsx";
import {
createStyles,
lighten,
makeStyles,
Theme,
} from "@material-ui/core/styles";
import CircularProgress from "@material-ui/core/CircularProgress";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TablePagination from "@material-ui/core/TablePagination";
import TableRow from "@material-ui/core/TableRow";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import Tooltip from "@material-ui/core/Tooltip";
import DeleteIcon from "@material-ui/icons/Delete";
import FilterListIcon from "@material-ui/icons/FilterList";
import axios, { AxiosResponse } from "axios";
import { getTask } from "../../service/merchants";
const baseUrl = "http://185.185.126.15:8080/api";
interface OnboardingTaskDto {
id?: number;
name: string;
}
async function getTask(
page: number,
size: number
): Promise<AxiosResponse<OnboardingTaskDto[]>> {
return await axios.get<OnboardingTaskDto[]>(
`${baseUrl}/management/onboarding/task?page=${page}&size=${size}`
);
}
interface Data {
id: number;
businessName: string;
title: string;
status: string;
}
function createData(
id: number,
businessName: string,
title: string,
status: string
): Data {
return { id, businessName, title, status };
}
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
if (b[orderBy] < a[orderBy]) {
return -1;
}
if (b[orderBy] > a[orderBy]) {
return 1;
}
return 0;
}
type Order = "asc" | "desc";
function getComparator<Key extends keyof any>(
order: Order,
orderBy: Key
): (
a: { [key in Key]: number | string },
b: { [key in Key]: number | string }
) => number {
return order === "desc"
? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy);
}
function stableSort<T>(array: T[], comparator: (a: T, b: T) => number) {
const stabilizedThis = array.map((el, index) => [el, index] as [T, number]);
stabilizedThis.sort((a, b) => {
const order = comparator(a[0], b[0]);
if (order !== 0) return order;
return a[1] - b[1];
});
return stabilizedThis.map((el) => el[0]);
}
interface HeadCell {
disablePadding: boolean;
id: keyof Data;
label: string;
numeric: boolean;
}
const headCells: HeadCell[] = [
{ id: "id", numeric: false, disablePadding: true, label: "id" },
{
id: "businessName",
numeric: true,
disablePadding: false,
label: "businessName",
},
{ id: "title", numeric: true, disablePadding: false, label: "title" },
{ id: "status", numeric: true, disablePadding: false, label: "status" },
];
interface EnhancedTableProps {
classes: ReturnType<typeof useStyles>;
numSelected: number;
onRequestSort: (
event: React.MouseEvent<unknown>,
property: keyof Data
) => void;
onSelectAllClick: (event: React.ChangeEvent<HTMLInputElement>) => void;
order: Order;
orderBy: string;
rowCount: number;
}
function EnhancedTableHead(props: EnhancedTableProps) {
const {
classes,
onSelectAllClick,
order,
orderBy,
numSelected,
rowCount,
onRequestSort,
} = props;
const createSortHandler =
(property: keyof Data) => (event: React.MouseEvent<unknown>) => {
onRequestSort(event, property);
};
return (
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
numSelected > 0 && numSelected < rowCount
}
checked={rowCount > 0 && numSelected === rowCount}
onChange={onSelectAllClick}
inputProps={{ "aria-label": "select all desserts" }}
/>
</TableCell>
{headCells.map((headCell) => (
<TableCell
key={headCell.id}
align={headCell.numeric ? "right" : "left"}
padding={headCell.disablePadding ? "none" : "normal"}
sortDirection={orderBy === headCell.id ? order : false}
>
<TableSortLabel
active={orderBy === headCell.id}
direction={orderBy === headCell.id ? order : "asc"}
onClick={createSortHandler(headCell.id)}
>
{headCell.label}
{orderBy === headCell.id ? (
<span className={classes.visuallyHidden}>
{order === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
))}
</TableRow>
</TableHead>
);
}
const useToolbarStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1),
},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(
theme.palette.secondary.light,
0.85
),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
title: {
flex: "1 1 100%",
},
})
);
interface EnhancedTableToolbarProps {
numSelected: number;
onClick: (e: React.MouseEvent<unknown>) => void;
}
const EnhancedTableToolbar = (props: EnhancedTableToolbarProps) => {
const classes = useToolbarStyles();
const { numSelected } = props;
return (
<Toolbar
className={clsx(classes.root, {
[classes.highlight]: numSelected > 0,
})}
>
{numSelected > 0 ? (
<Typography
className={classes.title}
color="inherit"
variant="subtitle1"
component="div"
>
{numSelected} selected
</Typography>
) : (
<Typography
className={classes.title}
variant="h6"
id="tableTitle"
component="div"
>
Customers
</Typography>
)}
{numSelected > 0 ? (
<Tooltip title="Delete">
<IconButton aria-label="delete" onClick={props.onClick}>
<DeleteIcon />
</IconButton>
</Tooltip>
) : (
<Tooltip title="Filter list">
<IconButton aria-label="filter list">
<FilterListIcon />
</IconButton>
</Tooltip>
)}
</Toolbar>
);
};
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: "100%",
},
paper: {
width: "100%",
marginBottom: theme.spacing(2),
},
table: {
minWidth: 750,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
})
);
export default function BusinessCustomersTable() {
const classes = useStyles();
const [order, setOrder] = React.useState<Order>("asc");
const [orderBy, setOrderBy] = React.useState<keyof Data>("businessName");
const [selected, setSelected] = React.useState<number[]>([]);
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(5);
const [rows, setRows] = useState<Data[]>([]);
const [loading, setLoading] = useState(false);
let updatedState: Data[] = [];
// TODO - move this to API file
const apiUrl = "http://185.185.126.15:8080/api/management/onboarding/task";
useEffect(() => {
const getData = async () => {
setLoading(true);
getTask(1, 100)
.then((resp) => {
console.log(resp.data);
})
.catch((error) => {
console.error(error);
});
const response = await axios.get(apiUrl, {
params: { page: 1, size: 100 },
});
setLoading(false);
const objContent: any = response.data.content;
for (let a = 0; a < objContent.length; a++) {
updatedState[a] = createData(
objContent[a].id,
objContent[a].businessName,
objContent[a].title,
objContent[a].status
);
setRows([...rows, ...updatedState]);
}
};
getData();
}, []);
const handleRequestSort = (
event: React.MouseEvent<unknown>,
property: keyof Data
) => {
const isAsc = orderBy === property && order === "asc";
setOrder(isAsc ? "desc" : "asc");
setOrderBy(property);
};
const handleSelectAllClick = (
event: React.ChangeEvent<HTMLInputElement>
) => {
if (event.target.checked) {
const newSelecteds = rows.map((n) => n.id);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event: React.MouseEvent<unknown>, id: number) => {
const selectedIndex = selected.indexOf(id);
let newSelected: number[] = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, id);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const handleDeleteClick = async () => {
// npm install qs
var qs = require("qs");
const response = await axios.delete(apiUrl, {
params: {
ids: selected,
},
paramsSerializer: (params) => {
return qs.stringify(params);
},
});
if (response.status === 204) {
const updatedData = rows.filter(
(row) => !selected.includes(row.id)
); // It'll return all data except selected ones
setRows(updatedData); // reset rows to display in table.
}
};
const isSelected = (id: number) => selected.indexOf(id) !== -1;
const emptyRows =
rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage);
return (
<div className={classes.root}>
<Paper className={classes.paper}>
<EnhancedTableToolbar
numSelected={selected.length}
onClick={handleDeleteClick}
/>
<TableContainer>
<Table
className={classes.table}
aria-labelledby="tableTitle"
aria-label="enhanced table"
>
<EnhancedTableHead
classes={classes}
numSelected={selected.length}
order={order}
orderBy={orderBy}
onSelectAllClick={handleSelectAllClick}
onRequestSort={handleRequestSort}
rowCount={rows.length}
/>
<TableBody>
{loading ? (
<div className="spinerr">
<CircularProgress />
</div>
) : null}
{stableSort(rows, getComparator(order, orderBy))
.slice(
page * rowsPerPage,
page * rowsPerPage + rowsPerPage
)
.map((row, index) => {
const isItemSelected = isSelected(row.id);
const labelId = `enhanced-table-checkbox-${index}`;
return (
<TableRow
hover
onClick={(event) =>
handleClick(event, row.id)
}
role="checkbox"
aria-checked={isItemSelected}
tabIndex={-1}
key={row.businessName}
selected={isItemSelected}
>
<TableCell padding="checkbox">
<Checkbox
checked={isItemSelected}
inputProps={{
"aria-labelledby":
labelId,
}}
/>
</TableCell>
<TableCell
component="th"
id={labelId}
scope="row"
padding="none"
>
{row.id}
</TableCell>
<TableCell align="right">
{row.businessName}
</TableCell>
<TableCell align="right">
{row.title}
</TableCell>
<TableCell align="right">
{row.status}
</TableCell>
</TableRow>
);
})}
{emptyRows > 0 && (
<TableRow style={{ height: 53 * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={rows.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
</Paper>
</div>
);
}
Sandbox: https://stackblitz.com/edit/react-ts-tnpk85?file=Hello.tsx
When data is loaded first time and I switch pages I don't see additional requests to Back end. Looks like data table rows data is loaded only once. I need to implement a lazy pagination and load current page data when I switch page. Do you know how I can fix this?
What @NafazBenzema is suggesting will probably work, but you have to keep in mind that by his suggestion you will be doing only frontend pagination, which is most likely not what you wanna do.
Frontend pagination means that:
This approach is not necessarily wrong, but it's not ideal either because it's not scalable. Moreover, it seems that you already have backend pagination in place! So you should try to use it 😃
To do so, you will:
total
(EDIT: your API does return this value, and it's called totalElements
in the response).page
and the rowsPerPage
to the useEffect
dependency array (the second parameter, which in your code is an empty array []
). This means that whenever the value of page
or rowsPerPage
change, the code inside the effect will be run again.Here is more or less how it should look:
export default function BusinessCustomersTable() {
const classes = useStyles();
const [order, setOrder] = React.useState<Order>("asc");
const [orderBy, setOrderBy] = React.useState<keyof Data>("businessName");
const [selected, setSelected] = React.useState<number[]>([]);
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(5);
const [rows, setRows] = useState<Data[]>([]);
const [loading, setLoading] = useState(false);
// TODO - move this to API file
const apiUrl = "http://185.185.126.15:8080/api/management/onboarding/task";
// Your API returns a property called `totalElements`.
// We need to set pass it to the 'count' prop of the TablePagination component to do server side pagination.
// Ref.: https://mui.com/api/table-pagination/
const [totalRows, setTotalRows] = useState(0);
useEffect(() => {
axios.get(apiUrl, {
params: { page, size: rowsPerPage },
})
.then((response) => {
setRows(response.data.content);
// As I said above, your response should return what's the total amount of rows.
// In your API response, that value is called `totalElements`.
setTotalRows(response.data.totalElements);
setLoading(false);
})
.catch((error) => {
console.error(error);
setLoading(false);
});
}, [page, rowsPerPage]); // Whenever the current 'page' or the amount of 'rowsPerPage' change, your request will be fired again.
const handleRequestSort = (
event: React.MouseEvent<unknown>,
property: keyof Data
) => {
const isAsc = orderBy === property && order === "asc";
setOrder(isAsc ? "desc" : "asc");
setOrderBy(property);
};
const handleSelectAllClick = (
event: React.ChangeEvent<HTMLInputElement>
) => {
if (event.target.checked) {
const newSelecteds = rows.map((n) => n.id);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event: React.MouseEvent<unknown>, id: number) => {
const selectedIndex = selected.indexOf(id);
let newSelected: number[] = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, id);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const handleDeleteClick = async () => {
// npm install qs
var qs = require("qs");
const response = await axios.delete(apiUrl, {
params: {
ids: selected,
},
paramsSerializer: (params) => {
return qs.stringify(params);
},
});
if (response.status === 204) {
const updatedData = rows.filter(
(row) => !selected.includes(row.id)
); // It'll return all data except selected ones
setRows(updatedData); // reset rows to display in table.
}
};
const isSelected = (id: number) => selected.indexOf(id) !== -1;
const emptyRows =
rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage);
return (
<div className={classes.root}>
<Paper className={classes.paper}>
<EnhancedTableToolbar
numSelected={selected.length}
onClick={handleDeleteClick}
/>
<TableContainer>
<Table
className={classes.table}
aria-labelledby="tableTitle"
aria-label="enhanced table"
>
<EnhancedTableHead
classes={classes}
numSelected={selected.length}
order={order}
orderBy={orderBy}
onSelectAllClick={handleSelectAllClick}
onRequestSort={handleRequestSort}
rowCount={rows.length}
/>
<TableBody>
{loading ? (
<div className="spinerr">
<CircularProgress />
</div>
) : null}
{stableSort(rows, getComparator(order, orderBy))
.map((row, index) => {
const isItemSelected = isSelected(row.id);
const labelId = `enhanced-table-checkbox-${index}`;
return (
<TableRow
hover
onClick={(event) =>
handleClick(event, row.id)
}
role="checkbox"
aria-checked={isItemSelected}
tabIndex={-1}
key={row.businessName}
selected={isItemSelected}
>
<TableCell padding="checkbox">
<Checkbox
checked={isItemSelected}
inputProps={{
"aria-labelledby":
labelId,
}}
/>
</TableCell>
<TableCell
component="th"
id={labelId}
scope="row"
padding="none"
>
{row.id}
</TableCell>
<TableCell align="right">
{row.businessName}
</TableCell>
<TableCell align="right">
{row.title}
</TableCell>
<TableCell align="right">
{row.status}
</TableCell>
</TableRow>
);
})}
{emptyRows > 0 && (
<TableRow style={{ height: 53 * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
component="div"
count={totalRows} // This is what your request should be returning in addition to the current page of rows.
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
</Paper>
</div>
);
}
Further unrelated things to consider:
useEffect
is written might give you some errors if you unmount the component before the request has finished (if you navigate somewhere else in the page for example). That's because it will be trying to set the state (setLoading
and setRows
) of an unmounted component. To guard against this scenario you could follow this other answer.rowsPerPage
it seems that you are also resetting the page to be the first one. This will cause the effect, and thus the request, be fired twice. You could instead either not reset the page to be the first one, or keep the state of the current page and the rowsPerPage inside one common useState
hook, and update always both at once.