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

huangapple go评论77阅读模式
英文:

How to avoid "Too many re-renders. React limits the number of renders to prevent an infinite loop."

问题

我正在使用React TypeScript,Redux Toolkit和Material UI。在调用API时遇到以下错误:

> 错误:重新渲染次数过多。React限制渲染次数以防止无限循环。
在renderWithHooks (http://127.0.0.1:5173/node_modules/.vite/deps/chunk-QJV3R4PZ.js?v=8a99eba5:12178:23)处
在mountIndeterminateComponent (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
// 其他导入...
/* ++++ Redux 导入 ++++ */
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "src/redux";
import { roleActions } from "../roles/RolesActions";
/* ---- Redux 导入 ---- */

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);
    }
  }

  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">角色权限</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" }}
          >
            保存
          </Button>
        </Grid>
      </Grid>
    </Box>
  );
};

export default EditMenuPermission;

RoleMenuTrees.tsx

// 其他导入
/* ++++ Redux 导入 ++++ */
import { useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "src/redux";
import { roleActions } from "src/features/admin/roles/RolesActions";
/* ---- Redux 导入 ---- */
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]);

  /*++++ 合并 roleMenus + allMenus 开始 +++++*/
  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);

  /*---- 合并 roleMenus + allMenus 结束 ----*/

  const createRoleMenuTree = useRoleMenuTree(
    mergedMenus as unknown as SingleRoleMenuDTO[]
  );
  const tree = createRoleMenuTree.tree;
  const mapMenu = createRoleMenuTree.mapMenu;

  return (
    <Box>
      <Box sx={{ backgroundColor: "#fafafa" }}>
        {/*++++ 菜单列表开始 ++++*/}
        <TreeView
          className="TreeView"
          defaultExpandIcon={
            <ChevronRightIcon sx={{ fontSize: "1.5rem !important" }} />
          }
          defaultCollapseIcon={
            <ExpandMoreIcon sx={{ fontSize: "1.5rem !important" }} />
          }
        >
          {tree?.map((data) => (
            <Box

<details>
<summary>英文:</summary>

I am using react typescript, redux toolkit and material UI. I am getting this error in while calling the API:
&gt; Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
    at renderWithHooks (http://127.0.0.1:5173/node_modules/.vite/deps/chunk-QJV3R4PZ.js?v=8a99eba5:12178:23)
    at mountIndeterminateComponent (http://127.0.0.1:5173/node_modules/.vite/deps/chunk-QJV3R4PZ.js?v=8a99eba5:14921:21)
    at beginWork (http://127.0.0.1:5173/node_modules/.vite/deps/chunk-QJV3R4PZ.js?v=8a99eba5:15902:22)....
   

I am providing my code below:

**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(&quot;updated Menus:&quot;, 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;


I tried removing the dependencies in useEffect. But the error still persists. 
</details>
# 答案1
**得分**: 2
# 问题
问题在于在 React 组件生命周期之外意外地进行了 React 状态更新,作为一种无意的副作用。这段代码在每次 `EditMenuPermission` 组件渲染时都会被调用,如果 `role.menus` 为真值,将会排队一个状态更新并触发组件重新渲染。这就是你看到的渲染循环。
```typescript
const role = useSelector((state: RootState) =&gt; state.roles.selected) as Role;
const [roleMenus, setRoleMenus] = useState&lt;RoleMenuItem[]&gt;([]);
if (role?.menus) {
try {
const parsedMenus = JSON.parse(role.menus) as RoleMenuItem[];
setRoleMenus(parsedMenus);
} catch (error) {
console.error(&quot;Error parsing role menus:&quot;, error);
}
}

解决方案

roleMenus 状态更新移到组件生命周期中。

幼稚的解决方案

一个幼稚的方法是使用 useEffect 钩子将 roleMenus 状态与当前的 role.menus 值同步。

const role = useSelector((state: RootState) =&gt; state.roles.selected) as Role;
const [roleMenus, setRoleMenus] = useState&lt;RoleMenuItem[]&gt;([]);

useEffect(() =&gt; {
  if (role?.menus) {
    try {
      const parsedMenus = JSON.parse(role.menus) as RoleMenuItem[];
      setRoleMenus(parsedMenus);
    } catch (error) {
      console.error(&quot;Error parsing role menus:&quot;, error);
    }
  }
}, [role?.menus]);

改进的解决方案 1

这会起作用,但通常被认为是 React 的反模式,将派生的“状态”存储在 React 状态中。当前的 roleMenus 值可以轻松地从当前的 role.menus 值计算出来。您应该记住,如果您发现自己编写了一个 useState/useEffect 耦合,这通常是您应该改用 useMemo 钩子的时候。

const role = useSelector((state: RootState) =&gt; state.roles.selected) as Role;

const roleMenus = useMemo&lt;RoleMenuItem[]&gt;(() =&gt; {
  try {
    return JSON.parse(role.menus) as RoleMenuItem[];
  } catch (error) {
    console.error(&quot;Error parsing role menus:&quot;, error);
    return [];
  }
}, [role?.menus]);

改进的解决方案 2

如果这是您经常从 Redux 中选择和计算的内容,我建议考虑将逻辑移到选择器函数中。

示例:

const selectRoleMenus = (state: RootState) =&gt; {
  const role = state.roles.selected;

  try {
    return JSON.parse(role.menus) as RoleMenuItem[];
  } catch (error) {
    console.error(&quot;Error parsing role menus:&quot;, error);
    return [];
  }
};
const role = useSelector((state: RootState) =&gt; state.roles.selected) as Role;
const roleMenus = useSelector(selectRoleMenus) as RoleMenuItem[]; // 注意多了一个分号

进一步改进的建议

更好的方法是在更新 Redux 状态时,将角色数据在切片减少函数中仅进行一次 JSON 解析,而不是在读取状态时每次进行计算。

英文:

Issue

The issue here is enqueueing a React state update outside the React component lifecycle as an unintentional side-effect. This code is called any time the EditMenuPermission component renders, and if role.menus is truthy, will enqueue a state update and trigger the component to rerender. This is the render looping you see.

const role = useSelector((state: RootState) =&gt; state.roles.selected) as Role;
const [roleMenus, setRoleMenus] = useState&lt;RoleMenuItem[]&gt;([]);

if (role?.menus) {
  try {
    const parsedMenus = JSON.parse(role.menus) as RoleMenuItem[];
    setRoleMenus(parsedMenus);
  } catch (error) {
    console.error(&quot;Error parsing role menus:&quot;, error);
  }
}

Solution

Move the roleMenus state update into the component lifecycle.

Naive Solution

A naive approach would be to use the useEffect hook to synchronize the roleMenus state to the current role.menus value.

const role = useSelector((state: RootState) =&gt; state.roles.selected) as Role;
const [roleMenus, setRoleMenus] = useState&lt;RoleMenuItem[]&gt;([]);

useEffect(() =&gt; {
  if (role?.menus) {
    try {
      const parsedMenus = JSON.parse(role.menus) as RoleMenuItem[];
      setRoleMenus(parsedMenus);
    } catch (error) {
      console.error(&quot;Error parsing role menus:&quot;, error);
    }
  }
}, [role?.menus]);

Improved Solution 1

This would work, but it's generally considered a React anti-pattern to store derived "state" into React state. The current roleMenus value is easily computed from the current role.menus value. You should keep in mind that just about 100% of the time if you find you've coded a useState/useEffect coupling that this is when you should use the useMemo hook instead.

const role = useSelector((state: RootState) =&gt; state.roles.selected) as Role;

const roleMenus = useMemo&lt;RoleMenuItem[]&gt;(() =&gt; {
  try {
    return JSON.parse(role.menus) as RoleMenuItem[];
  } catch (error) {
    console.error(&quot;Error parsing role menus:&quot;, error);
    return [];
  }
}, [role?.menus]);

Improved Solution 2

If this is something you select and compute often from Redux, I'd suggest considering moving the logic into a selector function.

Example:

const selectRoleMenus = (state: RootState) =&gt; {
  const role = state.roles.selected;

  try {
    return JSON.parse(role.menus) as RoleMenuItem[];
  } catch (error) {
    console.error(&quot;Error parsing role menus:&quot;, error);
    return [];
  }
};
const role = useSelector((state: RootState) =&gt; state.roles.selected) as Role;
const roleMenus = useSelector(selectRoleMenus) as RoleMenuItem[];;

Further Improved Suggestion

And better still, just JSON.parse the role data in the slice reducer function when updating the Redux state so the computation is done only once each time the state is updated instead of each time the state is read.

答案2

得分: 1

在高层次上,if 块中的 setState 看起来是问题的来源。

尝试将其移动到一个带有 role.menus 作为依赖的 useEffect 中。

像下面的快照所示:

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[]>([]);

  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]);

  ...
英文:

On high level the setState in the if block seems to be the culptrit.

Try moving it to a useEffect with role.menus as a dependency.

Like the snapshot shown

const EditMenuPermission = () =&gt; {
const { id } = useParams();
const [selected, setSelected] = useState&lt;RoleMenuItem[]&gt;(
[] as RoleMenuItem[]
);
const [selectedIds, setSelectedIds] = useState&lt;number[]&gt;([] as number[]);
const role = useSelector((state: RootState) =&gt; state.roles.selected) as Role;
const [roleMenus, setRoleMenus] = useState&lt;RoleMenuItem[]&gt;([]);
useEffect(() =&gt; {
if (role?.menus) {
try {
const parsedMenus = JSON.parse(role.menus) as RoleMenuItem[];
setRoleMenus(parsedMenus);
} catch (error) {
console.error(&quot;Error parsing role menus:&quot;, error);
}
}
}, [role?.menus]);
...

huangapple
  • 本文由 发表于 2023年6月18日 17:59:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/76499964.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定