Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

useFormik cause too many re-renders

Stack:

  • NextTS
  • Formik
  • Redux Toolkit
  • MUI5
  • Yup

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:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

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.

>Solution :

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.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading