搜尋

首頁  >  問答  >  主體

如何避免“重新渲染次數過多。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粉009186469240 天前364

全部回覆(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
  • 取消回覆