如何解决React TypeScript中‘Too many re-renders’无限循环错误
问题
使用React TypeScript、Redux Toolkit和Material UI开发时,调用API出现以下错误:
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
at renderWithHooks (...)
at mountIndeterminateComponent (...)
at beginWork (...)....
已尝试移除useEffect中的依赖项,但错误仍然存在,相关代码如下:
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;
解决方案
1. 修复EditMenuPermission.tsx的核心循环问题
直接在组件函数体内调用setRoleMenus会触发无限渲染:每次组件渲染时,只要role?.menus存在就会执行setRoleMenus,进而触发新的渲染循环。将这段逻辑移到useEffect中,仅在role?.menus变化时执行:
// 替换原有的if(role?.menus)代码块 useEffect(() => { if (role?.menus) { try { const parsedMenus = JSON.parse(role.menus) as RoleMenuItem[]; setRoleMenus(parsedMenus); } catch (error) { console.error("Error parsing role menus:", error); } } }, [role?.menus]);
2. 清理useEffect的无效依赖
原useEffect中监听role?.id会导致不必要的触发,因为role是Redux状态,其任意属性变化都会触发该effect。移除无效依赖,仅保留必要项:
useEffect(() => { dispatch(roleActions.findOne(id as unknown as number)); }, [dispatch, id]);
3. 优化RoleMenuTrees.tsx的计算性能(可选)
mergedMenus在每次组件渲染时都会重新计算,用useMemo缓存结果,避免不必要的计算:
// 替换原mergedMenus计算代码 const mergedMenus = useMemo(() => { return 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; } }); }, [allMenus, roleMenus]);
4. 移除调试日志(可选)
频繁的console.log会增加渲染负担,调试完成后建议移除:
// console.log("previousMenus:", roleMenus, "selected:", selected); // console.log("mergedMenus:", mergedMenus);
内容的提问来源于stack exchange,提问作者Emdadul Haque
相关产品推荐
相关产品推荐

