Infinite loop in Tree Selector

40 Views Asked by At

I wrote a tree selector component that its job is to render a tree object and it can take preSelected ids and if user change the selection it should give back the new selected ids. it looks like this :

// import { useState } from 'react';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { Box, Checkbox } from '@mui/material';
import { TreeItem, TreeView } from '@mui/lab';
import { useEffect, useState } from 'react';

export interface RenderTree {
    id: string;
    title: string;
    children?: RenderTree[];
}

interface Props {
    data: RenderTree,
    selected?: string[]
    onSelect: any;
}


const TreeViewSelector: React.FC<Props> = ({ data, selected, onSelect }) => {
    const [userInteraction, setUserInteraction] = useState<boolean>(true);
    const [selectedIds, setSelectedIds] = useState<string[]>([])


    useEffect(() => {
        if (selected && selected?.length > 0) {
            setSelectedIds(selected);
        }
    }, [selected]);

    useEffect(() => {
        if (userInteraction) {
            onSelect(selectedIds);
        }
    }, [onSelect, selectedIds, userInteraction]);

    // recursive function
    const findNode = (targetId: string, tree: RenderTree): RenderTree | undefined => {
        if (targetId === tree.id) {
            return tree;
        }
        if (tree.children) {
            for (const child of tree.children) {
                const foundObj = findNode(targetId, child)
                if (foundObj) {
                    return foundObj
                }
            }
        }
        return undefined
    }

    // recursive function
    const getAllChildrenIds = (selectedNode: RenderTree, currentIds: string[] = []) => {
        const updatedIds = [...currentIds, selectedNode.id];

        if (selectedNode.children) {
            selectedNode.children.forEach((child: RenderTree) => {
                updatedIds.push(...getAllChildrenIds(child, updatedIds));
            });
        }

        return [...new Set(updatedIds)];
    };



    const handleSelectCheckBox = (e: any) => {
        e.stopPropagation();
        setUserInteraction(false);
        const nodeId = e.target.value;
        const isChecked = e.target.checked;

        //handling when a node gets checked it's children gets checked too
        if (isChecked) {
            const selectedNode = findNode(nodeId, data)
            const childrenIds = getAllChildrenIds(selectedNode!)
            setSelectedIds(prev => {
                const updatedIds = [...prev];

                childrenIds.forEach(id => {
                    if (updatedIds.indexOf(id) === -1) {
                        updatedIds.push(id);
                    }
                });

                return updatedIds;
            });
        }



        // handleing when a single node gets checked it removes or adds from selectedIds
        setSelectedIds((prev) => {
            if (e.target.checked) {
                if (prev.findIndex((id) => id === nodeId) === -1) {
                    return [...prev, nodeId]
                }
                return prev;
            } else {
                return prev.filter((id) => id !== nodeId)
            }

        })

        setUserInteraction(true);
    }


    const renderTree = (nodes: RenderTree) => (
        <TreeItem
            sx={{
                '& .MuiTreeItem-content:hover': {
                    borderRadius: '6px',
                },
                '& .MuiTreeItem-content': {
                    borderRadius: '6px',
                },
            }}
            key={nodes.id}
            nodeId={nodes.id}
            label={
                <>
                    <Checkbox
                        size='small'
                        className='p-0.5'
                        onClick={handleSelectCheckBox}
                        value={nodes.id}
                        checked={selectedIds.includes(nodes.id)}
                    />
                    <span className='text-xs'>{nodes.title}</span>
                </>
            }
        >
            {Array.isArray(nodes.children)
                ? nodes.children.map((node) => renderTree(node))
                : null}
        </TreeItem>
    );

    return (
        <Box sx={{ height: "full", flexGrow: 1, width: "full", overflow: "auto" }} >
            <TreeView
                defaultCollapseIcon={<ExpandMoreIcon />}
                defaultExpanded={['root']}
                defaultExpandIcon={<ChevronRightIcon />}
            >
                {renderTree(data)}
            </TreeView>
        </Box>
    );
}

export default TreeViewSelector

I use this TreeSelector in a parent component that i give it a selecetd and onSelect like this:

import React, { useCallback, useEffect, useMemo, useState } from "react";
import Button from "@components/utils/button/button.component";
import { useGetBusinessUnitsQuery } from "@/infrastructure/slice/business-unit.slice";
import Loading from "@/components/utils/loading.component";
import styled from "styled-components";
import EmptyList from "@/components/utils/empty-list.component";
import { BusinessUnitItem } from "@/@types";
import TreeViewSelector, { RenderTree } from "@/components/utils/new features/tree-view-selector.component.test";


const Container = styled.aside`
  .scrollbar-container {
    ::-webkit-scrollbar {
      width: 8px;
      background-color: #ececec;
    }

    ::-webkit-scrollbar-thumb {
      border-radius: 6px;
      background-color: #a5a5a5;
    }
  }
`;

interface Props {
  data?: string[];
  onPrev: () => void;
  onDone?: (data: { businessUnits: string[] }) => void;
  onNext: (data: { businessUnits: string[] }) => void;
  isSubmitting?: boolean;
}

const AuditFormBusinessUnit: React.FC<Props> = ({
  data: serverData,
  onPrev,
  onDone,
  onNext,
  isSubmitting,
}) => {
  // const [initialized, setInitialized] = useState<boolean>(false);
  const [selected, setSelected] = useState<string[]>([]);

  const onSubmit = () => {
    if (onDone && selected) {
      onDone({ businessUnits: selected || [] });
    }
  };


  const handleNext = () => {
    if (selected) {
      onNext({ businessUnits: selected || [] });
    }
  };

  const { data: units, isLoading } = useGetBusinessUnitsQuery();

  const transformData = useCallback((node: BusinessUnitItem): RenderTree => {
    const transformedNode: RenderTree = {
      id: String(`S_${node.id}`),
      title: node.title,
      children: []
    };

    if (node.subLevel && node.subLevel.length > 0) {
      transformedNode.children = node.subLevel.map(transformData);
    }

    if (node.users && node.users.length > 0) {
      transformedNode.children = [
        ...(transformedNode.children || []),
        ...node.users.map((user) => ({
          id: String(`U_${user.id}`),
          title: `${user.firstName} ${user.lastName}`,
        })),
      ];
    }

    return transformedNode;
  }, []);

  const data = useMemo(() => {
    if (units && units[0]) {
      return transformData(units[0])
    }
    return { id: '', title: '', children: [] } as RenderTree;
  }, [transformData, units])

  const handleSelected = (selectedIds: string[]) => {
    console.log("selectedIds", selectedIds)
    const updatedData = selectedIds.filter((item) => item.includes("U")).map((id) => id.split("_")[1])
    console.log("updatedData", updatedData)

    setSelected(updatedData)
  }



  useEffect(() => {
    if (serverData) {
      setSelected(serverData)
    }
  }, [serverData])
  

  return (
    <>
      {isLoading ? (
        <Loading />
      ) : (
        <>
          <div className="flex flex-col justify-between w-full h-4/5">
            <Container>
              <div className="border flex flex-col rounded border-slate-500 w-full mx-auto h-[45vh]">
                {units && units?.length > 0 ? (
                  <section className="overflow-y-auto scrollbar-container">
                    <div className="flex-1 w-full p-2">
                      <TreeViewSelector data={data} onSelect={handleSelected} selected={selected} />
                    </div>
                  </section>
                ) : (
                  <EmptyList label="واحد سازمانی یافت نشد." />
                )}
              </div>
            </Container>
            <div className="flex items-center justify-between mt-5">
              <Button color="light" type="button" onClick={onPrev}>
                مرحله قبل
              </Button>
              <div className="flex items-center gap-4">
                {onDone && (
                  <Button
                    onClick={onSubmit}
                    disabled={!selected}
                    loading={isSubmitting}
                  >
                    ثبت نهایی
                  </Button>
                )}
                <Button
                  onClick={handleNext}
                  disabled={!selected}
                >
                  ثبت و ادامه
                </Button>
              </div>
            </div>
          </div>
        </>
      )}
    </>
  );
};

export default AuditFormBusinessUnit;


but i got an infinite re-renders in the parent component and I cant find the solution for it. I think it is caused by how I update selected and selectedIds.

can anyone give me an solution for this?

0

There are 0 best solutions below