skip to Main Content

I am using react typescript, redux toolkit and material UI. I am getting this error in while calling the API:

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("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 dependencies in useEffect. But the error still persists.

2

Answers


  1. 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 = () => {
      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]);
    
      ...
    
    Login or Signup to reply.
  2. 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) => 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 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) => 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 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) => 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 select and compute often from Redux, I’d suggest 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[];;
    

    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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search