我正在尝试构建一个treeview组件,以做出反应,根据用户扩展的节点来获取树的数据。
想法
扩展第一个节点时,会将HTTP请求发送到服务,该服务返回该节点的所有子代。扩展另一个节点时,将获取该节点的子节点,等等。我有一个非常大的数据集,因此我更喜欢这种获取方法,而不是在网站启动时获取所有数据。
问题
这可能是扩展 division 节点时服务返回的数据的示例
{
"division": {
"id": "1234",
"name": "string",
"address": "string",
},
"children": [
{
"id": "3321",
"parentId": "1234",
"name": "Marketing",
"address": "homestreet",
},
{
"id": "3323",
"parentId": "1234",
"name": "Development",
"address": "homestreet",
}
]
}
然后我可以例如。展开开发节点,并获取该节点的子代。
我在想我需要某种嵌套数组,但是我不确定如何处理维护数组的正确顺序,以便获得正确的树层次结构。因为用户可以选择扩展任何节点。有人可以帮忙吗?
答案 0 :(得分:2)
我认为我的原始答案(以下,因为已被接受)是反模式的一个例子,或者没有考虑问题的根本。
React组件树本身是...一棵树。因此,如果您只有一个TreeNode
组件或知道如何加载其子组件的类似组件,您会很好:
function TreeNode({id, name, parentId, address}) {
// The nodes, or `null` if we don't have them yet
const [childNodes, setChildNodes] = useState(null);
// Flag for whether this node is expanded
const [expanded, setExpanded] = useState(false);
// Flag for whether we're fetching child nodes
const [fetching, setFetching] = useState(false);
// Flag for whether child node fetch failed
const [failed, setFailed] = useState(false);
// Toggle our display of child nodes
const toggleExpanded = useCallback(
() => {
setExpanded(!expanded);
if (!expanded && !childNodes && !fetching) {
setFailed(false);
setFetching(true);
fetchChildNodes(id)
.then(nodes => setChildNodes(nodes.map(node => <TreeNode {...node} />)))
.catch(error => setFailed(true))
.finally(() => setFetching(false));
}
},
[expanded, childNodes, fetching]
);
return (
<div class="treenode">
<input type="button" onClick={toggleExpanded} value={expanded ? "-" : "+"} style={{width: "28px"}}/>
<span style={{width: "4px", display: "inline-block"}}></span>{name}
{failed && expanded && <div className="failed">Error fetching child nodes</div>}
{fetching && <div className="loading">Loading...</div>}
{!failed && !fetching && expanded && childNodes && (childNodes.length > 0 ? childNodes : <div class="none">(none)</div>)}
</div>
);
}
带有伪造的ajax和数据的实时示例:
const {useState, useCallback} = React;
const fakeData = {
"1234": {
id: "1234",
name: "Division",
address: "string",
childNodes: ["3321", "3323"]
},
"3321": {
id: "3321",
parentId: "1234",
name: "Marketing",
address: "homestreet",
childNodes: ["3301", "3302"]
},
"3301": {
id: "3301",
parentId: "3321",
name: "Promotion",
address: "homestreet",
childNodes: []
},
"3302": {
id: "3302",
parentId: "3321",
name: "Advertising",
address: "homestreet",
childNodes: ["3311", "3312"]
},
"3311": {
id: "3311",
parentId: "3302",
name: "Television",
address: "homestreet",
childNodes: []
},
"3312": {
id: "3312",
parentId: "3302",
name: "Social Media",
address: "homestreet",
childNodes: []
},
"3323": {
id: "3323",
parentId: "1234",
name: "Development",
address: "homestreet",
childNodes: ["3001", "3002", "3003", "3004"]
},
"3001": {
id: "3001",
parentId: "3323",
name: "Research",
address: "homestreet",
childNodes: []
},
"3002": {
id: "3002",
parentId: "3323",
name: "Design",
address: "homestreet",
childNodes: []
},
"3003": {
id: "3003",
parentId: "3323",
name: "Coding",
address: "homestreet",
childNodes: []
},
"3004": {
id: "3004",
parentId: "3323",
name: "Testing",
address: "homestreet",
childNodes: []
},
};
function fakeAjax(url) {
return new Promise((resolve, reject) => {
const match = /\d+/.exec(url);
if (!match) {
reject();
return;
}
const [id] = match;
setTimeout(() => {
if (Math.random() < 0.1) {
reject(new Error("ajax failed"));
} else {
resolve(fakeData[id].childNodes.map(childId => fakeData[childId]));
}
}, Math.random() * 400);
});
}
function fetchChildNodes(id) {
return fakeAjax(`/get/childNodes/${id}`);
}
function TreeNode({id, name, parentId, address}) {
// The nodes, or `null` if we don't have them yet
const [childNodes, setChildNodes] = useState(null);
// Flag for whether this node is expanded
const [expanded, setExpanded] = useState(false);
// Flag for whether we're fetching child nodes
const [fetching, setFetching] = useState(false);
// Flag for whether child node fetch failed
const [failed, setFailed] = useState(false);
// Toggle our display of child nodes
const toggleExpanded = useCallback(
() => {
setExpanded(!expanded);
if (!expanded && !childNodes && !fetching) {
setFailed(false);
setFetching(true);
fetchChildNodes(id)
.then(nodes => setChildNodes(nodes.map(node => <TreeNode {...node} />)))
.catch(error => setFailed(true))
.finally(() => setFetching(false));
}
},
[expanded, childNodes, fetching]
);
return (
<div class="treenode">
<input type="button" onClick={toggleExpanded} value={expanded ? "-" : "+"} style={{width: "28px"}}/>
<span style={{width: "4px", display: "inline-block"}}></span>{name}
{failed && expanded && <div className="failed">Error fetching child nodes</div>}
{fetching && <div className="loading">Loading...</div>}
{!failed && !fetching && expanded && childNodes && (childNodes.length > 0 ? childNodes : <div class="none">(none)</div>)}
</div>
);
}
ReactDOM.render(
<TreeNode {...fakeData["1234"]} />,
document.getElementById("root")
);
.treenode > .treenode,
.treenode > .none {
margin-left: 32px;
}
.failed {
color: #d00;
}
.none {
font-style: italics;
color: #aaa;
}
<div>This includes a 1 in 10 chance of any "ajax" operation failing, so that can be tested. Just collapse and expand again to re-try</div>
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>
...我不再想太多了。 :-)
我在想我需要某种嵌套数组,但是不确定如何维护数组的正确顺序,以便获得正确的树层次结构。
每个节点应该大致上如您所显示的那样(但作为一个组件),但children
是节点对象本身的属性,而不是数组中的单独对象。我也将使用Map
而不是数组,因为Map
提供了顺序(如数组)和 键检索(通过id
)。因此,带有扩展的division
节点的结构将如下所示(用JavaScript表示,而不是JSON):
this.state.treeRoot = new Map([
[
"1234",
<TreeNode
id="1234"
name="Division"
address="string"
children={new Map([
[
"3321",
<TreeNode
id="3321"
parentId="1234"
name="Marketing"
address="homestreet"
children={null}
/>
],
[
"3323",
<TreeNode
id="3323"
parentId="1234"
name="Development"
address="homestreet"
children={null}
/>
]
])
}
]
]);
我在这里使用null
作为标志值来表示“我们还没有尝试扩展子级”。空的Map
将是“我们扩展了子项,但没有子项。” :-)(您可以使用undefined
而不是null
,甚至可以不使用children
属性,但是我更喜欢保持节点的形状一致[有助于JavaScript引擎优化]并在以后要有对象的地方使用null
。
因为用户可以选择扩展任何节点。
您已经显示了具有唯一id
值的节点,因此这不成问题。确保将id
传递给处理扩展节点的处理程序,或者更好的是传递id
s的路径。
由于状态必须在React中是不可变的,因此您需要通过更新其children
属性来处理克隆到要修改的节点的每个容器。
例如,这是一个函数的草图(仅是草图!),该函数接收id
值的路径:
async function retrieveChildren(path) {
const children = await doAjax(`/path/to/${path[path.length - 1].id}`);
await new Promise((resolve, reject) => { // Ugh, but setState callback is non-promise, so...
this.setState(({treeRoot}) => {
treeRoot = new Map(treeRoot);
let node = treeRoot;
for (const id of path) {
node = node.children && node.children.get(id);
if (!node) {
reject(new Error(`No node found for path ${path}`));
return;
}
node = {...node, children: node.children === null ? null : new Map(node.children)};
}
node.children = new Map(children.map(child => [child.id, <TreeNode {...child} parent={node} />]);
return {treeRoot};
}, resolve);
});
}
如果使用钩子,它将非常相似:
const [treeRoot, setTreeRoot] = useState(new Map());
// ...
async function retrieveChildren(path) {
const children = await doAjax(`/path/to/${path[path.length - 1].id}`);
await new Promise((resolve, reject) => { // Ugh, but setState callback is non-promise, so...
setTreeRoot(treeRoot => {
treeRoot = new Map(treeRoot);
let node = treeRoot;
for (const id of path) {
node = node.children && node.children.get(id);
if (!node) {
reject(new Error(`No node found for path ${path}`));
return;
}
node = {...node, children: node.children === null ? null : new Map(node.children)};
}
node.children = new Map(children.map(child => [child.id, <TreeNode {...child} parent={node} />]);
return treeRoot;
}, resolve);
});
}
假设children
作为子节点数组返回。
答案 1 :(得分:2)
我会让树的每个节点成为一个单独的组件,称为“ TreeNode”或其他东西,那么它必须持有的是:
在这方面,“树”组件应仅包含单个属性:
然后,当用户单击某个TreeNode时,它将触发一个事件,TreeNode将对其进行处理:
此设置意味着您只需要2种类型的组件,而无需任何嵌套数组。
每个TreeNode组件负责如何呈现其自己的有效负载数据,然后负责其所有子组件,因此它必须知道如何应用视觉样式等。但这对于任何可见的react组件都是一样的。 / p>
所以,重点是每个节点仅负责更深一层的直接子节点,而与子孙等发生的事情保持不可知状态。
更新:
一个小警告:每个TreeNode必须停止鼠标单击向上冒泡。在SO上对此有一个专门的问题: How to stop even propagation in react
更新2
此外,有两种方法可以保存每个节点的提取数据:
在父组件中保留所有子数据,包括有效载荷(不仅仅是ID)。在这种情况下,上面的“其子级ID列表”将变为“其子级数据列表:ID和有效载荷”
所有提取的数据都被“全局”保存,例如在Tree组件中。然后,每个TreeNode在渲染孩子时都必须引用该知识源并通过ID检索数据。
在这种情况下,Tree组件可以使用JS Map对象(ID-> NodeData)。
在第二种设置中,如果一个TreeNode根本不使用Tree组件,则每个TreeNode也应该保留对Tree或直接映射的引用。像这样的东西。
答案 2 :(得分:0)
我对React并不是特别熟悉,但是通常来说,每个节点都必须有一个children
元素,该元素将是一个空数组。我假设当用户扩展节点时,您知道它正在扩展哪个节点(当用户单击“扩展”按钮时,该节点对象可能对您可用),因此,获取子节点并替换空节点很简单。 children
包含服务器中数据的数组。