我正在使用 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 中的依赖项。但错误仍然存在。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号
问题
这里的问题是将 React 状态更新排入 React 组件生命周期外部,这是一种无意的副作用。每当
EditMenuPermission组件呈现时都会调用此代码,如果role.menus为真,则会将状态更新排入队列并触发组件重新呈现。这是您看到的渲染循环。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); } }解决方案
将
roleMenus状态更新移至组件生命周期中。简单的解决方案
一种简单的方法是使用
useEffect挂钩将roleMenus状态同步到当前role.menus值。const role = useSelector((state: RootState) => state.roles.selected) as Role; const [roleMenus, setRoleMenus] = useState<RoleMenuItem[]>([]); 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]);改进的解决方案1
这可行,但通常被认为是将派生的“状态”存储到 React 状态中的 React 反模式。当前的
roleMenus值可以很容易地从当前的role.menus值计算出来。您应该记住,如果您发现自己编写了useState/useEffect耦合,那么大约 100% 的情况下,您应该使用useMemo钩子代替。const role = useSelector((state: RootState) => state.roles.selected) as Role; const roleMenus = useMemo<RoleMenuItem[]>(() => { try { return JSON.parse(role.menus) as RoleMenuItem[]; } catch (error) { console.error("Error parsing role menus:", error); return []; } }, [role?.menus]);改进的解决方案2
如果这是您经常从 Redux 选择和计算的内容,我建议考虑将逻辑移至选择器函数中。
示例:
const selectRoleMenus = (state: RootState) => { const role = state.roles.selected; try { return JSON.parse(role.menus) as RoleMenuItem[]; } catch (error) { console.error("Error parsing role menus:", error); return []; } };进一步改进的建议
更好的是,在更新 Redux 状态时,只需 JSON.parse 切片缩减器函数中的角色数据,这样每次更新状态时只需执行一次计算,而不是每次读取状态时进行计算。