我正在使用 React TypeScript、Redux 工具包和 Material UI。我在调用 API 时遇到此错误:
错误:重新渲染次数过多。 React 限制渲染数量以防止无限循环。 在 renderWithHooks (http://127.0.0.1:5173/node_modules/.vite/deps/chunk-QJV3R4PZ.js?v=8a99eba5:12178:23) 在mountInminatedComponent(http://127.0.0.1:5173/node_modules/.vite/deps/chunk-QJV3R4PZ.js?v=8a99eba5:14921:21) 在beginWork(http://127.0.0.1:5173/node_modules/.vite/deps/chunk-QJV3R4PZ.js?v=8a99eba5:15902:22)....
我在下面提供我的代码:
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;
我尝试删除 useEffect 中的依赖项。但错误仍然存在。
问题
这里的问题是将 React 状态更新排入 React 组件生命周期外部,这是一种无意的副作用。每当
EditMenuPermission
组件呈现时都会调用此代码,如果role.menus
为真,则会将状态更新排入队列并触发组件重新呈现。这是您看到的渲染循环。解决方案
将
roleMenus
状态更新移至组件生命周期中。简单的解决方案
一种简单的方法是使用
useEffect
挂钩将roleMenus
状态同步到当前role.menus
值。改进的解决方案1
这可行,但通常被认为是将派生的“状态”存储到 React 状态中的 React 反模式。当前的
roleMenus
值可以很容易地从当前的role.menus
值计算出来。您应该记住,如果您发现自己编写了useState
/useEffect
耦合,那么大约 100% 的情况下,您应该使用useMemo
钩子代替。改进的解决方案2
如果这是您经常从 Redux 选择和计算的内容,我建议考虑将逻辑移至选择器函数中。
示例:
进一步改进的建议
更好的是,在更新 Redux 状态时,只需 JSON.parse 切片缩减器函数中的角色数据,这样每次更新状态时只需执行一次计算,而不是每次读取状态时进行计算。