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
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.
Store data in states, not components.
Use state for immuatables (strings, numbers, booleans) or data Objects or UI state flags (isOpen, activeTab, etc.)
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>
</>
)