How to avoid "Too many re-renders. React limits the number of renders to prevent infinite loops."
P粉009186469
P粉009186469 2024-04-01 19:21:53
0
1
399

I'm using React TypeScript, the Redux toolkit, and Material UI. I get this error when calling the API:

Error: Too many re-renders. React limits the number of renders to prevent infinite loops. in renderWithHooks (http://127.0.0.1:5173/node_modules/.vite/deps/chunk-QJV3R4PZ.js?v=8a99eba5:12178:23) In mountInminatedComponent (http://127.0.0.1:5173/node_modules/.vite/deps/chunk-QJV3R4PZ.js?v=8a99eba5:14921:21) In beginWork (http://127.0.0.1:5173/node_modules/.vite/deps/chunk-QJV3R4PZ.js?v=8a99eba5:15902:22)....

I provide 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("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;

I tried removing the dependency in useEffect. But the error still exists.

P粉009186469
P粉009186469

reply all(1)
P粉594941301

question

The problem here is that queuing React state updates into the React component lifecycle external is an unintentional side effect. This code is called whenever the EditMenuPermission component renders, and if role.menus is true, will enqueue the status update and trigger the component to re-render. This is the render loop you see.

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

solution

Move roleMenus status updates into the component life cycle.

Simple solution

A simple way is to use the useEffect hook to synchronize the roleMenus state to the current role.menus value.

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

Improved solution 1

This works, but is often considered a React anti-pattern of storing derived "state" into React state. The current roleMenus value can be easily calculated from the current role.menus value. You should keep in mind that if you find yourself writing a useState/useEffect coupling, then about 100% of the time you should use the useMemo hook instead.

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

Improved solution 2

If this is something you frequently select and calculate from Redux, I recommend considering moving the logic into a selector function.

Example:

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

Suggestions for further improvements

Even better, when updating the Redux state, just JSON.parse the role data in the slice reducer function, so that only one calculation is performed each time the state is updated instead of each time the state is read. .

Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template