2
votes

I get this error when trying to submit a Formik form.

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. in Formik (at layout.tsx:113) in div (created by ForwardRef(CardContent)) in ForwardRef(CardContent) (created by WithStyles(ForwardRef(CardContent))) in WithStyles(ForwardRef(CardContent)) (created by Context.Consumer) in StyledComponent (created by Styled(WithStyles(ForwardRef(CardContent)))) in Styled(WithStyles(ForwardRef(CardContent))) (created by CardContent) in CardContent (created by Context.Consumer) in StyledComponent (created by Styled(CardContent)) in Styled(CardContent) (at layout.tsx:112) in div (created by ForwardRef(Paper)) in ForwardRef(Paper) (created by WithStyles(ForwardRef(Paper))) in WithStyles(ForwardRef(Paper)) (created by ForwardRef(Card)) in ForwardRef(Card) (created by WithStyles(ForwardRef(Card))) in WithStyles(ForwardRef(Card)) (created by Context.Consumer) in StyledComponent (created by Styled(WithStyles(ForwardRef(Card)))) in Styled(WithStyles(ForwardRef(Card))) (created by Card) in Card (at layout.tsx:111)

The function onSubmit has no async calls:

<Formik
  ...
  onSubmit={(values, { setSubmitting }): void => {
    setTimeout(() => {
      // convert daysOfWeek to Weekday[]
      const daysOfWeek = values.daysOfWeek
        .filter(day => day.checked)
        .map(day => day.dayOfWeek)
      const edited: EditTask = { ...task, ...values, daysOfWeek }
      onEditTask(edited)
      setSubmitting(false)
    })
  }}
>

The onEditTask callback just updates the list of TaskItems.

The full TaskItem component looks like this. The '@web/core...' components are my own wrappers around Material UI components.

TaskItem.tsx

import React, { ReactElement } from 'react'
import { useTranslation } from 'react-i18next'
import Grid from '@material-ui/core/Grid'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import CloseIcon from '@material-ui/icons/Close'
import Typography from '@web/core/components/Typography'
import MenuButton from '@web/core/components/MenuButton'
import MenuItem from '@web/core/components/MenuItem'
import TextField from '@web/core/components/TextField'
import Checkbox from '@web/core/components/Checkbox'
import Button from '@web/core/components/Button'
import Card from '@web/core/components/Card'
import EditIcon from '@web/core/theme/icons/Edit'
import { TaskSchedule, Weekday } from 'stores/goals/models'
import {
  EditTask,
  getDistinctTaskSchedulesSorted,
} from 'components/CitizenDetails/DietRules/FormDrawer'
import { TaskItemViewMode } from 'components/CitizenDetails/DietRules/TaskItem'
import { Formik, FieldArray, Form } from 'formik'
import uuid from 'uuid/v4'
import * as Yup from 'yup'
import TaskOverview from '../TaskOverview'
import { DeleteIcon, DeleteMenuItemText, EditCardContent, RemoveScheduleButton } from './styled'

const taskValidationSchema = Yup.object().shape({
  subject: Yup.string().required('Required'),
  description: Yup.string().nullable(),
  daysOfWeek: Yup.array()
    .min(1, 'Min 1 day')
    .required('Required'),
  taskSchedules: Yup.array()
    .ensure()
    .min(0, 'Min 0 elements'),
})

interface Props {
  viewMode: TaskItemViewMode
  task: EditTask
  onChangeViewMode: (viewMode: TaskItemViewMode) => void
  onDeleteTask: () => void
  onEditTask: (task: EditTask) => void
  disableMenu?: boolean
}

export interface CheckedWeekday {
  checked: boolean
  dayOfWeek: Weekday
}

function mapToCheckedWeekday(weekday: Weekday, checked: boolean): CheckedWeekday {
  return {
    dayOfWeek: weekday,
    checked,
  }
}

export default function TaskItem({
  viewMode,
  task,
  onChangeViewMode,
  onDeleteTask,
  onEditTask,
  disableMenu,
}: Props): ReactElement {
  const [t] = useTranslation()

  type TaskFormValues = Pick<EditTask, 'subject' | 'description'> & {
    daysOfWeek: CheckedWeekday[]
    taskSchedules: TaskSchedule[]
  }

  const initialValues: TaskFormValues = {
    ...task,
    daysOfWeek: Object.values(Weekday).map(day =>
      mapToCheckedWeekday(day, task.daysOfWeek.includes(day))
    ),
    taskSchedules: getDistinctTaskSchedulesSorted(task.taskSchedules),
  }

  return (
    <>
      {viewMode === TaskItemViewMode.View ? (
        <Grid container justify="space-between" alignItems="center">
          <Grid item>
            <TaskOverview task={task} />
          </Grid>
          <Grid item>
            {!disableMenu && (
              <MenuButton id="diet-task-overview-menu-button">
                <MenuItem onClick={(): void => onChangeViewMode(TaskItemViewMode.Edit)}>
                  <ListItemIcon>
                    <EditIcon />
                  </ListItemIcon>
                  <ListItemText primary={t('edit')} />
                </MenuItem>
                <MenuItem key="diet-task-overview-menu-remove-button" onClick={onDeleteTask}>
                  <ListItemIcon>
                    <DeleteIcon />
                  </ListItemIcon>
                  <DeleteMenuItemText primary={t('remove')} />
                </MenuItem>
              </MenuButton>
            )}
          </Grid>
        </Grid>
      ) : (
        <Card>
          <EditCardContent>
            <Formik
              initialValues={initialValues}
              validationSchema={taskValidationSchema}
              onSubmit={(values, { setSubmitting }): void => {
                setTimeout(() => {
                  // convert daysOfWeek to Weekday[]
                  const daysOfWeek = values.daysOfWeek
                    .filter(day => day.checked)
                    .map(day => day.dayOfWeek)
                  const edited: EditTask = { ...task, ...values, daysOfWeek }
                  onEditTask(edited)
                  setSubmitting(false)
                })
              }}
            >
              {({
                values,
                errors,
                isValid,
                touched,
                dirty,
                handleChange,
                handleBlur,
                handleSubmit,
                handleReset,
                isSubmitting,
                validateForm,
              }): ReactElement => (
                <Form>
                  <Grid container direction="column" spacing={3}>
                    <Grid item>
                      <Typography variant="h6">{t('goals:diet.task.title')}</Typography>
                    </Grid>
                    <Grid item>
                      <TextField
                        id={`task-${task.uuid}-subject`}
                        label={t('title')}
                        name="subject"
                        onChange={handleChange}
                        onBlur={handleBlur}
                        value={values.subject}
                        fullWidth
                        inputProps={{ autoFocus: viewMode === TaskItemViewMode.Create }}
                      />
                      {errors.subject && touched.subject && errors.subject}
                    </Grid>
                    <Grid item>
                      <TextField
                        id={`task-${task.uuid}-description`}
                        label={t('description')}
                        name="description"
                        onChange={handleChange}
                        onBlur={handleBlur}
                        value={values.description}
                        rows="3"
                        multiline
                        fullWidth
                      />
                    </Grid>
                    <Grid item>
                      <Typography variant="subtitle1" gutterBottom>
                        {t('day').toUpperCase()}
                      </Typography>
                      <FieldArray
                        name="daysOfWeek"
                        render={({ replace }) => (
                          <Grid container spacing={2}>
                            {values.daysOfWeek.map((day, idx) => (
                              <Grid item key={`task-weekday-${day.dayOfWeek}`}>
                                <Checkbox
                                  id={`task-weekday-checkbox-${day.dayOfWeek}`}
                                  name={`daysOfWeek.${idx}.dayOfWeek`}
                                  value={day.dayOfWeek}
                                  checked={day.checked}
                                  label={t(`weekdays.${day.dayOfWeek.toLowerCase()}`, {
                                    context: 'short',
                                  })}
                                  onChange={e => {
                                    replace(idx, { ...day, checked: !day.checked })
                                  }}
                                />
                              </Grid>
                            ))}
                          </Grid>
                        )}
                      />
                    </Grid>

                    <FieldArray
                      name="taskSchedules"
                      render={({ remove, replace, push }) => (
                        <>
                          <Grid item>
                            <Typography variant="subtitle1" gutterBottom>
                              {t('schedule_plural').toUpperCase()}
                            </Typography>

                            {values.taskSchedules.map((schedule, idx) => (
                              <Grid
                                container
                                spacing={6}
                                key={`task-weekday-schedule-${schedule.uuid}`}
                              >
                                <Grid item>
                                  <TextField
                                    type="time"
                                    key={`task-schedule-${schedule.uuid}`}
                                    id={`task-schedule-${schedule.uuid}`}
                                    label={t('time')}
                                    name={`taskSchedules.${idx}.time`}
                                    value={schedule.time}
                                    onChange={e => {
                                      const time = e.target.value
                                      replace(idx, { ...schedule, time })
                                    }}
                                    inputProps={{
                                      step: 300, // 5 min
                                    }}
                                  />
                                </Grid>
                                <Grid item>
                                  <RemoveScheduleButton
                                    id={`task-weekday-button-remove-schedule-${schedule.uuid}`}
                                    vanilla
                                    icon
                                    onClick={(): void => {
                                      remove(idx)
                                      // bug in formik makes this necessary
                                      // see https://github.com/jaredpalmer/formik/issues/784#issuecomment-503135849
                                      setTimeout(() => {
                                        validateForm()
                                      }, 10)
                                    }}
                                  >
                                    <CloseIcon />
                                  </RemoveScheduleButton>
                                </Grid>
                              </Grid>
                            ))}
                          </Grid>
                          <Grid item>
                            <Button
                              id="task-weekday-button-add-schedule"
                              ghost
                              onClick={(): void => {
                                push({
                                  time: '00:00:00',
                                  taskUuid: task.uuid,
                                  uuid: uuid(),
                                  dayOfWeek: Weekday.Monday, // temporary value
                                })
                              }}
                            >
                              {t('goals:diet.task.add-schedule')}
                            </Button>
                          </Grid>
                        </>
                      )}
                    />

                    <Grid item>
                      <Grid container justify="flex-end" spacing={2}>
                        <Grid item>
                          <Button
                            id="task-button-cancel-editing"
                            size="small"
                            ghost
                            licorice
                            onClick={
                              viewMode === TaskItemViewMode.Create
                                ? onDeleteTask
                                : (): void => {
                                    handleReset()
                                    onChangeViewMode(TaskItemViewMode.View)
                                  }
                            }
                          >
                            {t('cancel')}
                          </Button>
                        </Grid>
                        <Grid item>
                          <Button
                            type="submit"
                            id="task-button-save-editing"
                            size="small"
                            disabled={!dirty || !isValid || isSubmitting}
                          >
                            {t('ok')}
                          </Button>
                        </Grid>
                      </Grid>
                    </Grid>
                  </Grid>
                </Form>
              )}
            </Formik>
          </EditCardContent>
        </Card>
      )}
    </>
  )
}
1

1 Answers

2
votes

Probably onEditTask is changing the viewMode, removing the <EditCardContent>, so when the call to setSubmitting happens Formik is already unmounted.