首页  >  问答  >  正文

如何避免“重新渲染次数过多。React 限制渲染次数以防止无限循环。”

我正在使用 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 中的依赖项。但错误仍然存​​在。

P粉009186469P粉009186469172 天前270

全部回复(1)我来回复

  • P粉594941301

    P粉5949413012024-04-02 13:20:08

    问题

    这里的问题是将 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 [];
      }
    };
    
    const role = useSelector((state: RootState) => state.roles.selected) as Role;
    const roleMenus = useSelector(selectRoleMenus) as RoleMenuItem[];;
    

    进一步改进的建议

    更好的是,在更新 Redux 状态时,只需 JSON.parse 切片缩减器函数中的角色数据,这样每次更新状态时只需执行一次计算,而不是每次读取状态时进行计算。

    回复
    0
  • 取消回复