useFormik 导致太多重新渲染

问题描述 投票:0回答:3

堆栈:

  • 下一个TS
  • 福米克
  • Redux 工具包
  • MUI5
  • 是的

我有一个用户页面,显示用户列表。此页面也有删除用户和编辑用户操作。编辑用户,将所选用户保存在 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
导致太多重新渲染错误。知道如何解决吗?谢谢。

redux next.js formik redux-toolkit
3个回答
2
投票

你不应该在渲染过程中调用像

formik.setValues
这样的副作用。这将导致另一个渲染,然后再次调用它。 相反,在
useEffect
.

中做这样的事情

所以,例如

useEffect(() => {
    formik.setValues({
      firstName: user.firstName,
      lastName: user.lastName,
      email: user.email
    });
}, [user])

每次

setValues
改变时都会调用
user
,但不会在那之外。


0
投票

@phry 在这种情况下,我们不需要将 formik conts 放在 useEffect 依赖项下吗?如果是这样,它会导致同样的问题,还是渲染循环?


0
投票

另一个修复方法是将表单用作组件并在其中初始化 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} />}
 )
}
© www.soinside.com 2019 - 2024. All rights reserved.