Stack:
I have a page User, which displays a list of users. This page has delete user and edit user actions as well. Edit user, saves the selected user in Redux state and opens an MUI5 SwipeableDrawer with a new component UserDetail. Inside the UserDetail page, the selectedUser is fetch using useState and set to the form enhanced with Formik. But it's failing with "Too many re-renders" error.
Here are the relevant files:
Users
component:
import React, { useState } from "react";
import {
Alert,
Box,
Button,
ButtonGroup,
CircularProgress,
Container,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Snackbar,
SwipeableDrawer,
Table,
TableBody,
TableCell,
TableContainer,
TableFooter,
TableHead,
TableRow,
} from "@mui/material";
import moment from "moment";
import { Delete, Edit, PersonAdd } from "@mui/icons-material";
import { useAppDispatch } from "services/hooks";
import { useRouter } from "next/router";
import { NextPage } from "next";
import {
useDeleteUserMutation,
useGetUsersQuery,
} from "../../services/UserService";
import Footer from "../../components/Footer/Footer";
import UserDetail from "./components/UserDetail";
import { UserType } from "../../services/types/UserType";
import { setUser } from "../../services/slices/UserSlice";
const EMPTY_DIALOG = {
open: false,
text: "",
title: "",
onConfirm: () => {},
onCancel: () => {},
};
const EMPTY_ALERT = {
open: false,
text: "",
};
const Users: NextPage = () => {
console.log("users");
const router = useRouter();
const dispatch = useAppDispatch();
const [offset, setOffset] = useState(0);
const [limit, setLimit] = useState(10);
const [dialog, setDialog] = useState(EMPTY_DIALOG);
const [alert, setAlert] = useState(EMPTY_ALERT);
const {
data,
error,
isLoading: isUsersLoading,
isSuccess: isUsersQueried,
isFetching: isUsersFetching,
isError: isUsersError,
} = useGetUsersQuery();
const [
deleteUser,
{ data: deletedUser, isLoading: isUserDeleting, isSuccess: isUserDeleted },
] = useDeleteUserMutation();
const drawerBleeding = 56;
const [openDrawer, setOpenDrawer] = React.useState(false);
const handleDeleteUser = (userId: number) => async () => {
try {
await deleteUser(userId).unwrap();
setAlert({
open: true,
text: `Successfully deleted user: ${userId}`,
});
resetDeleteDialog();
} catch (error) {
console.log(`Error: Failed deleting user with id ${userId}`);
}
};
const resetDeleteDialog = () => {
setDialog(EMPTY_DIALOG);
};
const openDeleteDialog = (userId: number) => () => {
setDialog({
open: true,
title: "Delete user",
text: `Delete user: ${userId}?`,
onConfirm: handleDeleteUser(userId),
onCancel: () => resetDeleteDialog(),
});
};
const resetAlert = () => {
setAlert(EMPTY_ALERT);
};
const editUser = (user: UserType) => () => {
setOpenDrawer(true);
dispatch(setUser(user));
};
const toggleEditDrawer = (newOpen: boolean) => () => {
setOpenDrawer(newOpen);
};
const renderTable = (users: UserType[], count: number) => {
const hasUsers = count > 0;
return (
<React.Fragment>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell colSpan={6} align="right">
<Button
variant="outlined"
color="primary"
onClick={toggleEditDrawer(true)}
>
<PersonAdd />
</Button>
</TableCell>
</TableRow>
<TableRow>
<TableCell>Id</TableCell>
<TableCell>First name</TableCell>
<TableCell>Last name</TableCell>
<TableCell>Email</TableCell>
<TableCell>Birth date</TableCell>
<TableCell></TableCell>
</TableRow>
</TableHead>
<TableBody>
{hasUsers ? (
users.map((user) => (
<TableRow key={user.id}>
<TableCell>{user.id}</TableCell>
<TableCell>{user.firstName}</TableCell>
<TableCell>{user.lastName}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>
{moment.utc(user.birthDate).format("MM-DD-YYYY")}
</TableCell>
<TableCell sx={{ textAlign: "right" }}>
<ButtonGroup>
<Button onClick={editUser(user)}>
<Edit />
</Button>
<Button onClick={openDeleteDialog(user.id)}>
{<Delete />}
</Button>
</ButtonGroup>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={6}>No users found.</TableCell>
</TableRow>
)}
</TableBody>
<TableFooter>
<TableRow>
{/*<TablePagination*/}
{/* component={TableCell}*/}
{/* count={count}*/}
{/* page={offset}*/}
{/* rowsPerPage={limit}*/}
{/* onChangePage={handleChangePage}*/}
{/* onChangeRowsPerPage={handleChangeRowsPerPage}*/}
{/*/>*/}
</TableRow>
</TableFooter>
</Table>
</TableContainer>
<SwipeableDrawer
anchor="bottom"
open={openDrawer}
onClose={toggleEditDrawer(false)}
onOpen={toggleEditDrawer(true)}
swipeAreaWidth={drawerBleeding}
disableSwipeToOpen={false}
ModalProps={{
keepMounted: true,
}}
>
<UserDetail toggleEditDrawer={toggleEditDrawer}></UserDetail>
</SwipeableDrawer>
</React.Fragment>
);
};
const renderBody = () => {
if (isUsersQueried) {
const { users, count } = data;
return isUsersFetching || isUsersLoading ? (
<Box sx={{ display: "flex" }}>
<CircularProgress />
</Box>
) : (
renderTable(users, count)
);
}
};
const renderError = () => {
return isUsersError && <Alert severity="error">{error}</Alert>;
};
return (
<Container maxWidth={"md"} fixed>
{renderError()}
{renderBody()}
<Footer></Footer>
<Dialog
open={dialog.open}
onClose={dialog.onCancel}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{dialog.title}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{dialog.text}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={dialog.onCancel}>Disagree</Button>
<Button onClick={dialog.onConfirm} autoFocus>
Agree
</Button>
</DialogActions>
</Dialog>
<Snackbar
open={alert.open}
autoHideDuration={6000}
onClose={resetAlert}
message={alert.text}
/>
</Container>
);
};
export default Users;
UserDetail
component:
import React, { useState } from "react";
import {
Alert,
Box,
Button,
Container,
Grid,
TextField,
Typography,
} from "@mui/material";
import { useSelector } from "react-redux";
import * as yup from "yup";
import { useFormik } from "formik";
import AdapterMoment from "@mui/lab/AdapterMoment";
import LocalizationProvider from "@mui/lab/LocalizationProvider";
import { DatePicker } from "@mui/lab";
import { NextPage } from "next";
import Footer from "../../../components/Footer/Footer";
import {
useCreateUserMutation,
useUpdateUserMutation,
} from "../../../services/UserService";
import { UserType } from "../../../services/types/UserType";
import { AppProps } from "next/app";
import { selectUser } from "../../../services/slices/UserSlice";
const validationSchema = yup.object({
email: yup
.string()
.trim()
.email("Please enter a valid email address")
.required("Email is required."),
firstName: yup.string().required("Please specify your first name"),
lastName: yup.string().required("Please specify your first name"),
birthDate: yup.date(),
});
const INITIAL_USER = {
firstName: "",
lastName: "",
email: "",
};
const UserDetail: NextPage = ({ toggleEditDrawer }: AppProps) => {
console.log("user detail");
const [birthDate, setBirthDate] = useState(null);
const [pageError, setPageError] = useState(null);
const user = useSelector(selectUser);
const [createUser, { isLoading: isUserCreating, isSuccess: isUserCreated }] =
useCreateUserMutation();
// you can get the detailed user if really needed
// const {
// data: user,
// isLoading: isUserLoading
// } = useGetUserQuery(user.id);
const [updateUser, { isLoading: isUserUpdating }] = useUpdateUserMutation();
const onSubmit = (values: UserType) => {
let newValues = {
...values,
birthDate: birthDate.toISOString(),
};
try {
if (user && user.id) {
newValues.id = user.id;
updateUser(newValues).unwrap();
} else {
createUser(newValues).unwrap();
}
} catch (error) {
setPageError(error);
} finally {
toggleEditDrawer(false)();
}
};
const formik = useFormik({
initialValues: INITIAL_USER,
validationSchema: validationSchema,
onSubmit,
});
const renderForm = () => {
console.log(user.birthDate);
setBirthDate(moment(user.birthDate));
// this part of the code causes the too many re-render error
formik.setValues({
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
});
return (
<form onSubmit={formik.handleSubmit}>
<Grid container spacing={4}>
<Grid item xs={12}>
<Typography variant={"subtitle2"} sx={{ marginBottom: 2 }}>
Enter your email
</Typography>
<TextField
label="Email *"
variant="outlined"
name={"email"}
fullWidth
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
/>
</Grid>
<Grid item xs={12}>
<Typography variant={"subtitle2"} sx={{ marginBottom: 2 }}>
Enter your firstname
</Typography>
<TextField
label="Firstname *"
variant="outlined"
name={"firstName"}
fullWidth
value={formik.values.firstName}
onChange={formik.handleChange}
error={
formik.touched.firstName && Boolean(formik.errors.firstName)
}
helperText={formik.touched.firstName && formik.errors.firstName}
/>
</Grid>
<Grid item xs={12}>
<Typography variant={"subtitle2"} sx={{ marginBottom: 2 }}>
Enter your lastName
</Typography>
<TextField
label="Lastname *"
variant="outlined"
name={"lastName"}
fullWidth
value={formik.values.lastName}
onChange={formik.handleChange}
error={formik.touched.lastName && Boolean(formik.errors.lastName)}
helperText={formik.touched.lastName && formik.errors.lastName}
/>
</Grid>
<Grid item xs={12}>
<Typography variant={"subtitle2"} sx={{ marginBottom: 2 }}>
Enter your birthdate
</Typography>
<LocalizationProvider dateAdapter={AdapterMoment}>
<DatePicker
label="Birthdate"
value={birthDate}
onChange={(newValue) => {
setBirthDate(newValue);
}}
renderInput={(params) => (
<TextField
{...params}
variant={"outlined"}
fullWidth
required
/>
)}
/>
</LocalizationProvider>
</Grid>
<Grid item xs={12}>
<Typography variant="subtitle2" gutterBottom>
Fields that are marked with * sign are required.
</Typography>
<Grid container spacing={2}>
<Grid item>
<Button
size="large"
variant="contained"
color="primary"
type={"submit"}
>
Save
</Button>
</Grid>
<Grid item>
<Button
size="large"
variant="contained"
color="secondary"
onClick={toggleEditDrawer(false)}
>
Cancel
</Button>
</Grid>
</Grid>
</Grid>
</Grid>
</form>
);
};
return (
<Container maxWidth={"md"}>
<Box sx={{ margin: 2 }}>
{pageError && <Alert severity="error">{pageError}</Alert>}
<Box marginBottom={4}>
<Typography
sx={{
textTransform: "uppercase",
fontWeight: "medium",
}}
gutterBottom
color={"text.secondary"}
>
Create User
</Typography>
<Typography color="text.secondary">Enter the details</Typography>
</Box>
</Box>
{user && renderForm()}
<Footer></Footer>
</Container>
);
};
export default UserDetail;
The code formik.setValues
causes the too many re-render error. Any idea how to solve it? Thanks.
You should never call a side effect such as formik.setValues
during render. That will lead to another render and then call it again.
Instead, do something like this in a useEffect
.
So, for example
useEffect(() => {
formik.setValues({
firstName: user.firstName,
lastName: user.lastName,
email: user.email
});
}, [user])
would call that setValues
every time user
changed, but not outside of that.