I am using react typescript, redux toolkit and material UI. I am getting this error in while calling the API:
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
at renderWithHooks (http://127.0.0.1:5173/node_modules/.vite/deps/chunk-QJV3R4PZ.js?v=8a99eba5:12178:23)
at mountIndeterminateComponent (http://127.0.0.1:5173/node_modules/.vite/deps/chunk-QJV3R4PZ.js?v=8a99eba5:14921:21)
at beginWork (http://127.0.0.1:5173/node_modules/.vite/deps/chunk-QJV3R4PZ.js?v=8a99eba5:15902:22)….
I am providing my code below:
EditMenuPermission.tsx
//EditMenuPermission.tsx
//other imports
/* ++++ Redux Imports ++++ */
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "src/redux";
import { roleActions } from "../roles/RolesActions";
/* ---- Redux Imports ---- */
const EditMenuPermission = () => {
const { id } = useParams();
const [selected, setSelected] = useState<RoleMenuItem[]>(
[] as RoleMenuItem[]
);
const [selectedIds, setSelectedIds] = useState<number[]>([] as number[]);
const role = useSelector((state: RootState) => state.roles.selected) as Role;
const [roleMenus, setRoleMenus] = useState<RoleMenuItem[]>([]);
if (role?.menus) {
try {
const parsedMenus = JSON.parse(role.menus) as RoleMenuItem[];
setRoleMenus(parsedMenus);
} catch (error) {
console.error("Error parsing role menus:", error);
}
}
const dispatch = useDispatch<AppDispatch>();
useEffect(() => {
dispatch(roleActions.findOne(id as unknown as number));
}, [dispatch, id, role?.id]);
console.log("previousMenus:", roleMenus, "selected:", selected);
const handleCreatePayload = async () => {
const updatedMenus = [...roleMenus];
selected.forEach((selectedItem) => {
const existingItemIndex = updatedMenus.findIndex(
(menu) => menu.id === selectedItem.id
);
if (existingItemIndex !== -1) {
updatedMenus[existingItemIndex] = selectedItem;
} else {
updatedMenus.push(selectedItem);
}
});
setRoleMenus(updatedMenus);
const payload = {
name: role.name,
is_active: true,
is_deleted: false,
menus: JSON.stringify(updatedMenus),
};
console.log("updated Menus:", updatedMenus);
const updateRole = await dispatch(roleActions.update(role.id, payload));
console.log(updateRole);
};
return (
<Box>
<AdminTitleContainer>
<AdminTitle variant="h5">Role Permission</AdminTitle>
</AdminTitleContainer>
<Grid container spacing={2}>
<Grid item xs={9}>
<Box>
<RoleMenuTrees
selected={selected}
setSelected={setSelected}
selectedIds={selectedIds}
setSelectedIds={setSelectedIds}
roleMenus={roleMenus}
/>
</Box>
</Grid>
<Grid item xs={3}>
<Button
variant="contained"
color="primary"
startIcon={<AddCircle />}
onClick={handleCreatePayload}
sx={{ position: "fixed" }}
>
Save
</Button>
</Grid>
</Grid>
</Box>
);
};
export default EditMenuPermission;
RoleMenuTrees.tsx
//other imports
/* ++++ Redux Imports ++++ */
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "src/redux";
import { roleActions } from "src/features/admin/roles/RolesActions";
/* ---- Redux Imports ---- */
import { useEffect } from "react";
import { useParams } from "react-router-dom";
import { useRoleMenuTree } from "src/hooks/useMenuTree";
import { SingleRoleMenuDTO } from "src/features/admin/roles/RolesDTO";
import { menuActions } from "src/features/admin/menu/MenuActions";
import {
AllMenu,
Permission,
PermissionType,
RoleMenuItem,
SingleRole,
} from "../../RoleDTO";
type RoleMenuTreesProp = {
selected: RoleMenuItem[];
setSelected: React.Dispatch<React.SetStateAction<RoleMenuItem[]>>;
selectedIds: number[];
setSelectedIds: React.Dispatch<React.SetStateAction<number[]>>;
roleMenus: RoleMenuItem[];
};
const RoleMenuTrees = ({
selected,
setSelected,
selectedIds,
setSelectedIds,
roleMenus,
}: RoleMenuTreesProp) => {
const dispatch = useDispatch<AppDispatch>();
const { id } = useParams();
const roleMenusJSON = useSelector(
(state: RootState) => state.roles.selected as SingleRole
)?.menus;
const allMenus = useSelector(
(state: RootState) => state.menus.list
) as AllMenu[];
useEffect(() => {
dispatch(menuActions.getList());
}, [dispatch, id]);
/*++++ merging roleMenus + allMenus starts +++++*/
const mergedMenus = allMenus?.map((menu) => {
const matchingMenu = roleMenus.find(
(roleMenu: RoleMenuItem) => roleMenu.id === menu.id
);
if (matchingMenu) {
const { permissions: _, ...rest } = { ...menu, ...matchingMenu };
return rest;
} else {
const permissions = JSON.parse(menu.permissions) as Permission[];
const permissionType = {} as PermissionType;
permissions?.forEach((permission) => {
const { key } = permission;
permissionType[key] = false;
});
const { permissions: _, ...rest } = {
...menu,
permission_type: permissions,
...permissionType,
};
return rest;
}
});
console.log("mergedMenus:", mergedMenus);
/*---- merging roleMenus + allMenus ends ----*/
const createRoleMenuTree = useRoleMenuTree(
mergedMenus as unknown as SingleRoleMenuDTO[]
);
const tree = createRoleMenuTree.tree;
const mapMenu = createRoleMenuTree.mapMenu;
return (
<Box>
<Box sx={{ backgroundColor: "#fafafa" }}>
{/*++++ Menu List starts ++++*/}
<TreeView
className="TreeView"
defaultExpandIcon={
<ChevronRightIcon sx={{ fontSize: "1.5rem !important" }} />
}
defaultCollapseIcon={
<ExpandMoreIcon sx={{ fontSize: "1.5rem !important" }} />
}
>
{tree?.map((data) => (
<Box key={data.id}>
<RoleMenuTree
data={data as unknown as RoleMenuItem}
selected={selected}
setSelected={setSelected}
selectedIds={selectedIds}
setSelectedIds={setSelectedIds}
mapMenu={mapMenu}
/>
</Box>
))}
</TreeView>
{/*---- Menu List ends ----*/}
</Box>
</Box>
);
};
export default RoleMenuTrees;
I tried removing the dependencies in useEffect. But the error still persists.
2
Answers
On high level the setState in the
if
block seems to be the culptrit.Try moving it to a
useEffect
withrole.menus
as a dependency.Like the snapshot shown
Issue
The issue here is enqueueing a React state update outside the React component lifecycle as an unintentional side-effect. This code is called any time the
EditMenuPermission
component renders, and ifrole.menus
is truthy, will enqueue a state update and trigger the component to rerender. This is the render looping you see.Solution
Move the
roleMenus
state update into the component lifecycle.Naive Solution
A naive approach would be to use the
useEffect
hook to synchronize theroleMenus
state to the currentrole.menus
value.Improved Solution 1
This would work, but it’s generally considered a React anti-pattern to store derived "state" into React state. The current
roleMenus
value is easily computed from the currentrole.menus
value. You should keep in mind that just about 100% of the time if you find you’ve coded auseState
/useEffect
coupling that this is when you should use theuseMemo
hook instead.Improved Solution 2
If this is something you select and compute often from Redux, I’d suggest considering moving the logic into a selector function.
Example:
Further Improved Suggestion
And better still, just JSON.parse the role data in the slice reducer function when updating the Redux state so the computation is done only once each time the state is updated instead of each time the state is read.