堆栈:
我有一个用户页面,显示用户列表。此页面也有删除用户和编辑用户操作。编辑用户,将所选用户保存在 Redux 状态,并打开一个带有新组件 UserDetail 的 MUI5 SwipeableDrawer。在 UserDetail 页面中,使用 useState 获取 selectedUser 并将其设置为使用 Formik 增强的表单。但它因“重新渲染太多”错误而失败。
以下是相关文件:
Users
成分:
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
成分:
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;
代码
formik.setValues
导致太多重新渲染错误。知道如何解决吗?谢谢。
你不应该在渲染过程中调用像
formik.setValues
这样的副作用。这将导致另一个渲染,然后再次调用它。
相反,在useEffect
.中做这样的事情
所以,例如
useEffect(() => {
formik.setValues({
firstName: user.firstName,
lastName: user.lastName,
email: user.email
});
}, [user])
每次
setValues
改变时都会调用user
,但不会在那之外。
@phry 在这种情况下,我们不需要将 formik conts 放在 useEffect 依赖项下吗?如果是这样,它会导致同样的问题,还是渲染循环?
另一个修复方法是将表单用作组件并在其中初始化 formik(这相当于您当前的
renderForm
函数:
const Page: NextPage = () => {
const user = selectUser
{...}
// You can extract this component also outside to the current module
const Form = ({onSubmit, user: {name, email}}) => (
const formik = useFormik({
initialValues: {
name,
email
},
validationSchema: validationSchema,
onSubmit
});
return (
<form>{...}</form>
)
)
return (
{...}
{user && <Form user={user} onSubmit={onSubmit} />}
)
}