动态添加到嵌套数组

时间:2019-12-07 10:59:19

标签: javascript reactjs

我正在尝试构建一个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",
   }
 ]
}

然后我可以例如。展开开发节点,并获取该节点的子代。

我在想我需要某种嵌套数组,但是我不确定如何处理维护数组的正确顺序,以便获得正确的树层次结构。因为用户可以选择扩展任何节点。有人可以帮忙吗?

3 个答案:

答案 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”或其他东西,那么它必须持有的是:

  • 其父代的ID(如果是根节点,则可以为空)
  • 其子ID列表
  • 显示是否已扩展的布尔属性
  • 最后是它自己的有效载荷数据(您的情况下的名称和地址)

在这方面,“树”组件应仅包含单个属性:

  • 其根节点的ID

然后,当用户单击某个TreeNode时,它将触发一个事件,TreeNode将对其进行处理:

  • 如果尚未扩展,则会开始扩展,从而导致获取请求。
  • 如果它已经被“扩展”了-那么这取决于您的要求-它可以折叠还是不能折叠-无论哪种情况都适合您的用例。

此设置意味着您只需要2种类型的组件,而无需任何嵌套数组。

每个TreeNode组件负责如何呈现其自己的有效负载数据,然后负责其所有子组件,因此它必须知道如何应用视觉样式等。但这对于任何可见的react组件都是一样的。 / p>

所以,重点是每个节点仅负责更深一层的直接子节点,而与子孙等发生的事情保持不可知状态。

更新:

一个小警告:每个TreeNode必须停止鼠标单击向上冒泡。在SO上对此有一个专门的问题: How to stop even propagation in react

更新2

此外,有两种方法可以保存每个节点的提取数据:

  1. 在父组件中保留所有子数据,包括有效载荷(不仅仅是ID)。在这种情况下,上面的“其子级ID列表”将变为“其子级数据列表:ID和有效载荷”

  2. 所有提取的数据都被“全局”保存,例如在Tree组件中。然后,每个TreeNode在渲染孩子时都必须引用该知识源并通过ID检索数据。

在这种情况下,Tree组件可以使用JS Map对象(ID-> NodeData)。

在第二种设置中,如果一个TreeNode根本不使用Tree组件,则每个TreeNode也应该保留对Tree或直接映射的引用。像这样的东西。

答案 2 :(得分:0)

我对React并不是特别熟悉,但是通常来说,每个节点都必须有一个children元素,该元素将是一个空数组。我假设当用户扩展节点时,您知道它正在扩展哪个节点(当用户单击“扩展”按钮时,该节点对象可能对您可用),因此,获取子节点并替换空节点很简单。 children包含服务器中数据的数组。