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?