I was given the task to build a recursive tree with some functionality. I managed to build a tree using a recursive component, I attach the code below.
function App() {
const [data, setData] = useState([{
id: 1,
name: "node 1",
children: [{
id: 2,
name: "node 1.1",
children: [{
id: 8,
name: "node 1.1.1",
children: []
}]
},{
id: 3,
name: "node 1.2",
children: [{
id: 4,
name: "node 1.2.1",
children: [{
id: 5,
name: "node 1.2.1.1",
children: []
}]
}]
}]
}])
return (
<div className="App">
<Tree data = {data} setData = {setData} margin={15}/>
<button>Add child</button>
</div>
);
}
and
const Tree = ({data, margin, setData}) => {
return (
<Fragment>
{data.map((element, index) => (
<div>
<div className="tier">
<div
style={{marginLeft: `${margin}px`}}
>
{element.name}
</div>
</div>
{element.children && element.children.length ? <Tree
data={element.children}
setData={setData}
margin={margin 15}
/> : false}
</div>))}
</Fragment>
);
};
I need to add a node selection when clicking on it. I have an idea about the implementation: create a state in which the ID of the desired node will be stored, and design it using CSS. But I have absolutely no idea how to add a child node to the selected node when the button is clicked.
CodePudding user response:
Nested array in your date tree you should map as well. It should look like that
{element.children?.map(el, I => (
<p key={I}> {el. name}</p>
{el.children?.map(child, ind => (
<p key={ind}>{child.name}</p>
)}
)}
the ?
is very important, because it would work only if there was a date like that.
CodePudding user response:
You did half of the job and your idea about storing id
is the best overall, you could actually store the selected item
itself but it will be not as great as it sounds due to mutation of this item, you will not be able to trigger dependent hooks update for it.
So you only need to store the id
of selected item and a few utility things to simplify the work with the tree
. On the code below you can see the flatTree
that was calculated with useMemo
from the original data. It just flattens your tree to an array, so you will be able to easilly find real selected item and to calculate the next id
that will be inserted on button click. And to add a few more props
to the Tree
component itself. onClick
to handle actual click and selectedTreeItem
just to add some classes for selected item (optional).
import "./styles.css";
import { Fragment, useEffect, useMemo, useState } from "react";
export default function App() {
const [data, setData] = useState([{id:1,name:"node 1",children:[{id:2,name:"node 1.1",children:[{id:8,name:"node 1.1.1",children:[]}]},{id:3,name:"node 1.2",children:[{id:4,name:"node 1.2.1",children:[{id:5,name:"node 1.2.1.1",children:[]}]}]}]}]);
const [selectedTreeItemId, setSelectedTreeItemId] = useState();
const flatTree = useMemo(() => {
const flat = (item) => [item, ...(item.children || []).flatMap(flat)];
return data.flatMap(flat);
}, [data]);
const selectedTreeItem = useMemo(() => {
if (!selectedTreeItemId || !flatTree) return undefined;
return flatTree.find((x) => x.id === selectedTreeItemId);
}, [flatTree, selectedTreeItemId]);
const getNextListItemId = () => {
const allIds = flatTree.map((x) => x.id);
return Math.max(...allIds) 1;
};
const onTreeItemClick = ({ id }) => {
setSelectedTreeItemId((curr) => (curr === id ? undefined : id));
};
const onAddChildClick = () => {
if (!selectedTreeItem) return;
if (!selectedTreeItem.children) selectedTreeItem.children = [];
const newObj = {
id: getNextListItemId(),
name: `${selectedTreeItem.name}.${selectedTreeItem.children.length 1}`,
children: []
};
selectedTreeItem.children.push(newObj);
// dirty deep-clone of the tree to force "selectedTreeItem"
// to be recalculated and its !reference! to be updated.
// so any hook that has a "selectedTreeItem" in depsArray
// will be executed now (as expected)
setData((curr) => JSON.parse(JSON.stringify(curr)));
};
useEffect(() => {
console.log("selectedTreeItem: ", selectedTreeItem);
}, [selectedTreeItem]);
return (
<div className="App">
<Tree
data={data}
margin={15}
onTreeItemClick={onTreeItemClick}
selectedTreeItem={selectedTreeItem}
/>
<button
type="button"
disabled={!selectedTreeItem}
onClick={onAddChildClick}
>
Add child
</button>
</div>
);
}
const Tree = ({ data, margin, onTreeItemClick, selectedTreeItem }) => {
return (
<Fragment>
{data.map((element) => (
<div key={element.id}>
<div
className={`tier ${
selectedTreeItem === element ? "selected-list-item" : ""
}`}
onClick={() => onTreeItemClick(element)}
>
<div style={{ marginLeft: `${margin}px` }}>
{element.id}: {element.name}
</div>
</div>
{element.children?.length > 0 && (
<Tree
data={element.children}
margin={margin 15}
onTreeItemClick={onTreeItemClick}
selectedTreeItem={selectedTreeItem}
/>
)}
</div>
))}
</Fragment>
);
};