代码之家  ›  专栏  ›  技术社区  ›  czetsuya

useFormik会导致太多重新渲染

  •  1
  • czetsuya  · 技术社区  · 4 年前

    堆栈:

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

    我有一个页面用户,它显示用户列表。此页面还包含删除用户和编辑用户操作。编辑用户,将所选用户保存为Redux状态,并打开带有新组件UserDetail的MUI5 SwipeableDrawer。在UserDetail页面中,selectedUser是使用useState获取的,并设置为使用Formik增强的表单。但它失败了,出现了“太多重新渲染”的错误。

    以下是相关文件:

    用户组件:

    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会导致太多重新渲染错误。知道怎么解决吗?谢谢

    1 回复  |  直到 4 年前
        1
  •  1
  •   phry    4 年前

    你不应该把副作用称为 formik.setValues 在渲染期间。这将导致另一个渲染,然后再次调用它。 相反,在一个特定的时间里做类似的事情 useEffect .

    比如说

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

    你会这么说吗 setValues 每一次 user 改变了,但不是在那之外。