有没有一种方法可以在React中迭代地渲染类别树结构?

huangapple go评论69阅读模式
英文:

Is there a way to render category tree structure in React iteratively?

问题

有没有一种方法可以在React中迭代性地呈现这个类别树结构?我目前正在将这个flatmap数组转化为一个类别树结构,但是似乎找不到如何迭代性地将其呈现到React中的方法?我只看到了使用递归的方法,但是我们的团队决定使用迭代,因为之前递归引起了一些内存问题。

我希望呈现这个结构的方式的图像

对于任何想法,将不胜感激。

  const flat = [
    { id: 1, parentId: 3 },
    { id: 3, parentId: 8 },
    { id: 4, parentId: 6 },
    { id: 6, parentId: 3 },
    { id: 7, parentId: 6 },
    { id: 8, parentId: null },
    { id: 16, parentId: null },
    { id: 10, parentId: 8 },
    { id: 15, parentId: 8 },
    { id: 13, parentId: 14 },
    { id: 14, parentId: 10 },
  ];

  const root = [];

  flat.forEach((node: any) => {
    if (!node.parentId) {
      return root.push(node);
    }

    const parentIndex = flat.findIndex((el: any) => el.id === node.parentId);
    if (!flat[parentIndex].children) {
      return (flat[parentIndex].children = [node]);
    }

    return flat[parentIndex].children.push(node);
  });

  console.log(root);

尝试迭代性地呈现类别树结构。

英文:

Is there a way to render this category tree structure in React iterratively? I'm currently making this flatmap array into a category tree structure iteratively, however, can't seem to find out how to render this to react iteratively? I only see a way out with recursion, but our team decided to use iteration as recursion caused some memory problems previously.

Image of the way I want this structure to render

Would be gratefull for any ideas.

  const flat = [
    { id: 1, parentId: 3 },
    { id: 3, parentId: 8 },
    { id: 4, parentId: 6 },
    { id: 6, parentId: 3 },
    { id: 7, parentId: 6 },
    { id: 8, parentId: null },
    { id: 16, parentId: null },
    { id: 10, parentId: 8 },
    { id: 15, parentId: 8 },
    { id: 13, parentId: 14 },
    { id: 14, parentId: 10 },
  ];

  const root = [];

  flat.forEach((node: any) => {
    if (!node.parentId) {
      return root.push(node);
    }

    const parentIndex = flat.findIndex((el: any) => el.id === node.parentId);
    if (!flat[parentIndex].children) {
      return (flat[parentIndex].children = [node]);
    }

    return flat[parentIndex].children.push(node);
  });

  console.log(root);

Tried to render category tree structure iteratively.

答案1

得分: 2

为了实现你想要的功能,你可以按照我在评论中描述的方式来处理树,将其展平为一个代表你要渲染内容的中序数组。为了做到这一点,你还需要跟踪遍历树所经过的路径,即父节点的列表。

这可能比较难理解,但这是我会做的方式。我在其中包含了一些描述。

从你的代码开始,首先让我们定义一个与你的树节点结构相匹配的根节点,因为现在你的根节点是一个数组,而节点是对象:

const tree = { children: root };

树结构在递归中更容易处理(具有讽刺意味的是,你希望跳过递归渲染,这本来会更容易)。为了展平树,让我们定义一个递归的reducer,它适用于你的树结构,类似于 Array.prototype.reduce,但适用于你的树结构。这个reduce函数基本上是基于这篇函数式JS文章

const reduceTree = (reducerFn) => (init, node) => {
  const acc = reducerFn(init, node);
  if (!node.children?.length) {
      return acc;
  }
  return node.children.reduce(reduceTree(reducerFn), acc);
}

这个看起来有点奇怪的函数是“柯里化”的,所以它首先接受一个reducerFn,然后返回一个函数,该函数本身接受reduce的参数。

现在我们可以展平树了,因为它按顺序迭代树,构建了一个节点ID的数组:

reduceTree((acc, node) => [...acc, node.id])([], tree)
// 生成 [undefined, 8, 3, 1, 6, 4, 7, 10, 14, 13, 15, 16]

注意第一个条目是 undefined,因为根节点没有ID。但这无关紧要,我们可以稍后过滤掉它。

现在的问题是如何获取到节点的路径。我能想到的最简单的方法是将路径作为参数添加到reduce函数中,就像map/reduce也会将索引作为参数传递一样。所以让我们修改reduce函数,以便传递路径:

const reduceTree = (reducerFn) => (init, node, path = []) => {
  // 通过删除路径中的当前节点的ID来移除当前节点的ID
  const acc = reducerFn(init, node, path.slice(0, -1));
  if (!node.children?.length) {
      return acc;
  }
  const reduce = reduceTree(reducerFn);
  return node.children.reduce((acc, node) => reduce(acc, node, [...path, node.id]), acc);
}

在最后一行,你可以看到我们将当前节点的ID连接到路径中,这就是现在将现有路径传递给reduce函数的方式。我们在参数列表中将其默认为 [],这样使用此reduce的用户就不必在开始时传入一个空路径。

现在让我们调用它!

reduceTree((acc, node, path) => [...acc, [path.join('-'), node.id]])([], tree);

如果你不习惯阅读reduce函数的签名,这可能会使它稍微容易阅读一些:

// 这个函数与现有的“累加器”一起“减少”当前节点。在这种情况下,我们正在构建一个嵌套的数组,通过获取到目前为止的内容("...acc")并添加当前路径和节点ID
const reduceFn = (acc, node, path) => [...acc, [path.join('-'), node.id]];
// 起始的累加器值
const initialValue = [];
// 做这个事情
reduceTree(reduceFn)(initialValue, tree);

这将产生:

[
  ['', undefined]
  ['', 8]
  ['8', 3]
  ['8-3', 1]
  ['8-3', 6]
  ['8-3-6', 4]
  ['8-3-6', 7]
  ['8', 10]
  ['8-10', 14]
  ['8-10-14', 13]
  ['8', 15]
  ['', 16]
]

现在你可以在你的React组件中进行迭代。跟踪缩进和/或更改路径结构留给你。

整个内容都在一起:

const flat = [
  { id: 1, parentId: 3 },
  { id: 3, parentId: 8 },
  { id: 4, parentId: 6 },
  { id: 6, parentId: 3 },
  { id: 7, parentId: 6 },
  { id: 8, parentId: null },
  { id: 16, parentId: null },
  { id: 10, parentId: 8 },
  { id: 15, parentId: 8 },
  { id: 13, parentId: 14 },
  { id: 14, parentId: 10 },
];

const root = [];

flat.forEach((node) => {
  if (!node.parentId) {
    return root.push(node);
  }

  const parentIndex = flat.findIndex((el) => el.id === node.parentId);
  if (!flat[parentIndex].children) {
    return (flat[parentIndex].children = [node]);
  }

  return flat[parentIndex].children.push(node);
});

const tree = { children: root };

const reduceTree = (reducerFn) => (init, node, path = []) => {
  const acc = reducerFn(init, node, path.slice(0, -1));
  if (!node.children?.length) {
      return acc;
  }
  const reduce = reduceTree(reducerFn);
  return node.children.reduce((acc, node) => reduce(acc, node, [...path, node.id]), acc);
}

reduceTree((acc, node, path) => [...acc, [path.join('-'), node.id]])([], tree);
英文:

To do what you want, you can do what I described in my comment, which is to flatten the tree into an in-order array representing what you to render. To do this, you also need to track the path through the tree you've taken, aka the list of parent nodes.

This will likely be hard to follow, but it's how I'd do it. I include descriptions along the way.

Starting from your code, first let's define a root node that matches you tree node shape, since right now your root node is an array, and the nodes are objects:

const tree = { children: root };

Tree structures are easier to deal with in recursion (ironically you want to skip recursive rendering, which would be easier). To flatten the tree, lets define a recursive reducer which works on your tree. Basically Array.prototype.reduce except for your tree structure. This reduce is loosely based on this functional JS article.

const reduceTree = (reducerFn) => (init, node) => {
  const acc = reducerFn(init, node);
  if (!node.children?.length) {
      return acc;
  }
  return node.children.reduce(reduceTree(reducerFn), acc);
}

This weird looking function is "curried", so it first takes a reducerFn, then returns a function that itself takes the reduce arguments.

This will let us flatten the tree now, as it iterates the tree in order, and builds up an array of node ids:

reduceTree((acc, node) => [...acc, node.id])([], tree)
// produces [undefined, 8, 3, 1, 6, 4, 7, 10, 14, 13, 15, 16]

Note the first entry is undefined because the root node has no ID. But who cares, we can filter that out later.

The issue/question is now how to get the path to the node. The easiest way I can think of is to add the path as an argument to the reducer itself, just like how map/reduce also pass the index as the argument. So let's modify the reduce function to also pass in the path:

const reduceTree = (reducerFn) => (init, node, path = []) => {
  // Remove the current node's ID from the path by removing the last
  // entry in the path
  const acc = reducerFn(init, node, path.slice(0, -1));
  if (!node.children?.length) {
      return acc;
  }
  const reduce = reduceTree(reducerFn);
  return node.children.reduce((acc, node) => reduce(acc, node, [...path, node.id]), acc);
}

On the last line, you can see that we concat the current node's id to the path, which is what now passes the existing path to the reducer function. We default it to [] in the argument list so that users of this reducer don't have to pass in an empty path to start.

Now let's call it!

reduceTree((acc, node, path) => [...acc, [path.join('-'), node.id]])([], tree);

If you aren't used to reading reduce function signatures, this might make it a little easier to read:

// The function that "reduces" the current node with the existing
// "accumulator". In this case we're building up a nested array by
// taking what we have so far ("...acc") and adding the current path
// and node id
const reduceFn = (acc, node, path) => [...acc, [path.join('-'), node.id]];
// The accumulator value to start with
const initialValue = [];
// Do the thing
reduceTree(reduceFn)(initialValue, tree);

which produces

[
['', undefined]
['', 8]
['8', 3]
['8-3', 1]
['8-3', 6]
['8-3-6', 4]
['8-3-6', 7]
['8', 10]
['8-10', 14]
['8-10-14', 13]
['8', 15]
['', 16]
]

And you can iterate over this now in you React component. Tracking indentation and/or changing the path structure is left up to you.

The whole thing, all together:

const flat = [
  { id: 1, parentId: 3 },
  { id: 3, parentId: 8 },
  { id: 4, parentId: 6 },
  { id: 6, parentId: 3 },
  { id: 7, parentId: 6 },
  { id: 8, parentId: null },
  { id: 16, parentId: null },
  { id: 10, parentId: 8 },
  { id: 15, parentId: 8 },
  { id: 13, parentId: 14 },
  { id: 14, parentId: 10 },
];

const root = [];

flat.forEach((node) => {
  if (!node.parentId) {
    return root.push(node);
  }

  const parentIndex = flat.findIndex((el) => el.id === node.parentId);
  if (!flat[parentIndex].children) {
    return (flat[parentIndex].children = [node]);
  }

  return flat[parentIndex].children.push(node);
});

const tree = { children: root };

const reduceTree = (reducerFn) => (init, node, path = []) => {
  const acc = reducerFn(init, node, path.slice(0, -1));
  if (!node.children?.length) {
      return acc;
  }
  const reduce = reduceTree(reducerFn);
  return node.children.reduce((acc, node) => reduce(acc, node, [...path, node.id]), acc);
}

reduceTree((acc, node, path) => [...acc, [path.join('-'), node.id]])([], tree);

huangapple
  • 本文由 发表于 2023年3月7日 15:21:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/75658998.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定