Search code examples
reactjsmaterial-uiformik

React: Formik doesn't trigger render when updating radio buttons from MUI


I'm working on a ToDo app using Formik and MUI. I switched from a select to radio buttons inside the Popover which opens when clicking the icons. But using radio buttons, it doesn't trigger a render when changing the values in there. I tried many other ways to handle it and to force-update a render. Nothing worked, so I put the current messy state of my code into a sandbox and hope anyone can help me with that.

https://codesandbox.io/p/sandbox/kfng7y

It should update the state and rerender the component to show the corret state, but it doesn't rerender and doesn't show the updated state


Solution

  • Ok, I just learned something after debugging for days-

    Putting JSX in a state creates a "state closure". They will "remember" the values that existed when they were created, not when they're rendered.

    TL;DR Any component that needs to stay reactive to changing values, needs to be rendered in JSX directly, not stored in a state.

    1. Store data in states, not components.

    2. Use state for immuatables (strings, numbers, booleans) or data Objects or UI state flags (isOpen, activeTab, etc.)

    3. Don't use state for JSX Elements, React Components and anything else that needs to access props or other changing values.

    So instead of putting JSX in a state like this:

    const TodoForm: React.FC<ToDoFormProps> = ({ onSubmit }) => {
      const [popoverContent, setPopoverContent] = useState<React.ReactNode>(null);
    
    // DON'T: Storing JSX in state that needs access to changing values
    const handleIconClick = (
        event: React.MouseEvent<HTMLElement>,
        content: React.ReactNode
      ) => {
        setAnchorEl(event.currentTarget);
        setPopoverContent(content);
      };
    
    

    You should keep the JSX in the return statement only. Never put it in a state:

    const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
      const [activePopover, setActivePopover] = useState<"date" | "tag" | "priority" | null>(null);
    
    // DO: Render components directly with current values
      const handleIconClick = (
        event: React.MouseEvent<HTMLElement>,
        type: "date" | "tag" | "priority"
      ) => {
        setAnchorEl(event.currentTarget);
        setActivePopover(type);
      };
    
    const handlePopoverClose = () => {
        setAnchorEl(null);
        setActivePopover(null);
      };
    
    
      // Render popover content based on active type
      const renderPopoverContent = (values: any, setFieldValue: any) => {
        switch (activePopover) {
          case "date":
            return (
              <LocalizationProvider dateAdapter={AdapterDateFns}>
                <DesktopDateTimePicker
                  ampm={false}
                  label="Fälligkeitsdatum"
                  value={values.dueDate}
                  onChange={(newValue) => {
                    setFieldValue("dueDate", newValue);
                  }}
                  minDate={new Date()}
                />
              </LocalizationProvider>
            );
          case "tag":
            return (
              <Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
                <RadioGroup
                  name="tag"
                  value={values.tag}
                  onChange={(e) => {
                    setFieldValue("tag", e.target.value);
                  }}
                >
                  {tags.map((tag) => (
                    <FormControlLabel
                      key={tag}
                      value={tag}
                      control={<Radio />}
                      label={tag}
                      sx={{
                        border: "1px solid #ccc",
                        borderRadius: 2,
                        p: 1,
                        m: 0.5,
                      }}
                    />
                  ))}
                </RadioGroup>
              </Box>
            );
          case "priority":
            return (
              <Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
                <RadioGroup
                  name="priority"
                  value={values.priority}
                  onChange={(e) => {
                    setFieldValue("priority", e.target.value);
                  }}
                >
                  <FormControlLabel
                    value="low"
                    control={<Radio />}
                    label="Niedrig"
                    sx={{
                      border: "1px solid #ccc",
                      borderRadius: 2,
                      p: 1,
                      m: 0.5,
                    }}
                  />
                  <FormControlLabel
                    value="medium"
                    control={<Radio />}
                    label="Mittel"
                    sx={{
                      border: "1px solid #ccc",
                      borderRadius: 2,
                      p: 1,
                      m: 0.5,
                    }}
                  />
                  <FormControlLabel
                    value="high"
                    control={<Radio />}
                    label="Hoch"
                    sx={{
                      border: "1px solid #ccc",
                      borderRadius: 2,
                      p: 1,
                      m: 0.5,
                    }}
                  />
                </RadioGroup>
              </Box>
            );
          default:
            return null;
        }
      };
    
    return (
               <>
               <Grid container spacing={1} alignItems="center">
                  <IconButton onClick={(e) => handleIconClick(e, "date")}>
                    <CalendarTodayIcon />
                  </IconButton>
                  <IconButton onClick={(e) => handleIconClick(e, "tag")}>
                    <TagIcon />
                  </IconButton>
                  <IconButton onClick={(e) => handleIconClick(e, "priority")}>
                    <PriorityHighIcon />
                  </IconButton>
                </Grid>
    
                <Popover
                  open={Boolean(anchorEl)}
                  anchorEl={anchorEl}
                  onClose={handlePopoverClose}
                  anchorOrigin={{
                    vertical: "bottom",
                    horizontal: "center",
                  }}
                  transformOrigin={{
                    vertical: "top",
                    horizontal: "center",
                  }}
                >
                  <Box sx={{ p: 2 }}>
                    {renderPopoverContent(values, setFieldValue)}
                  </Box>
                </Popover>
                </>
    
    
    )