简体   繁体   中英

Vue - Storing a tree in Pinia

The question I'm going to ask is about Pinia, but really could just be generalized to any underlying store.

I have a Vue + Pinia application in which I want to be able to store a tree. The tree is made up of objects of type Node . I need to store exactly one tree at a time, and I don't care about the root (we can imagine it's there, but what I care about is the root's children, their children, and so on).

I want to support the following operations:

  • create a new top-level node
  • create a node that's child of another node
  • modify or delete a node, regardless of whether it's top-level or child or child of a child
  • I want to be able to move around a node or an entire subtree, changing its parent or even just its position relative to its siblings
  • the whole tree won't be fetched from the backend at once; it'll be fetched lazily. You can imagine when a node is opened in the UI, its direct children are fetched and so on.

Here's something I have thought of doing:

  • keep in my store an array of Node s containing the top level nodes. Let's call it topLevelNodes
  • keep an object nodeIdToChildren , which maps the id of a node to an array of Node s that are its children

I would initially fetch the top level nodes, filling the array topLevelNodes .

For each node that needs to know its children, fetch them and put them in nodeIdToChildren under the parent id as key.

An advantage of this approach is that it's easy to add, delete, and move around nodes: just touch the relevant entries in the mapping. The biggest drawback is that it's much less efficient to just find a node, regardless of its position. Say I want to edit node with id xyz , not knowing whose child it is.

I could create a getter that flattens all the values in the mapping object together with the values in the top level nodes array, but I'm not sure about efficiency of that.

Are there any better ways of doing this?

The most efficient way is to store all nodes in a single array and in the children/parents array of a node you only place ids of other nodes.

  1. You should choose whether you store children or parents into an item, but not both. The principle is: you don't want to store the same information in two places, because if it goes out of sync you'll have subtle/weird bugs. For example, if you store children array of ids: you can have a computed parents , but it returns the ids of all items which currently contain the current item's id in their children array. Similarly, if you store parents, the children will be a computed.
  2. This flat array structure allows building two type of UI lists: top-down and bottom-up (you can provide both to the user, in two separate tabs).
  3. A good navigation system for trees is breadcrumb (pretty much like folders on a computer). The breadcrumb logic is relatively simple: When you navigate to an item, you pass its id to the breadcrumb. If it's already in the breadcrumb, it's an upwards navigation and you splice the breadcrumb to that item. If it's not, it's a downwards navigation, you add the item's id at the end of the breadcrumb.
  4. It also allows you to easily search for something in all ancestors of an item (no matter how many levels) or all descendants of an item (no matter how many levels). Note: if you allow cyclic relations (A > B > A), you have to account for that in your searches.

Notes :

  • If you want to lazy load, you'll have to move search and filtering on backend. If the total number of nodes is in the thousands, you should not lazy-load. You only request all nodes once, and then you can navigate & search through them without making another request. It won't affect performance. What does affect performance is rendering more nodes than you can display on a viewport, but that's a completely different subject. Another point to be made here: if nodes have heavy dependencies (eg: images), only lazy load those dependencies (eg: load those when you actually display the item, making a separate request (eg: getItemDetails ))
  • The above allows having both types of systems: the ones which allow cycles (A > B > A) or which don't allow. Each one has its own type of limitations (the first one needs limitation when recursively calculating parents/children); the second one has limitation to exclude ancestors from children selector and to exclude descendants from parents selector (assuming you build UI to change children/parents).
  • a big advantage is that child/parent relation between two nodes is only store in one place. (example: if you want to move a child from one parent to another you have to: remove its ID from the current parent's children array of ids, add its ID to the new parent's children . The child itself is not affected, except the value of its parents computed changes. If you're storing parents , - and children is computed - you need to change the child's parents array. And, of course, each of the old and new parent's children computed will change when you perform this change).
  • another advantage is it allows a maximum level of flexibility (any node can be child or parent of any other number of nodes simultaneously - which doesn't mean they have to: you can always limit the number of parents to one and you get the "classic" folder structure where any node can only have one direct parent).

You can see a working demo here . Sorry about the styles, I just tweaked something from a different sandbox. But you get the basic implementations of both <TreeView /> and <TableView /> . This one has 1k nodes but should have no issues up to 5k nodes. Above that, you need to be careful what you render and when.

This is not specified to Vue/Pinia so I just write the general idea, you can choose the way to implement it.

You will store your tree on a flat map.

const tree = new Map()

Each item will be a node and the key is the nodeId . Each node will contain these properties:

{
  id: "the node id",
  parentId: "id of the parent, null if it is the root node",
  childIds: "array of the id of its child",
  content: "content of the node, whatever you want"
}

Let's go through each operator you want:

  • create a new top-level node:
// difficulty: easy
const rootNode = {
  id: "nodeId"
  parentId: null,
  childIds: [...],
  content: "..."
}

tree.set(nodeId, rootNode)
  • create a node that's a child of another node
// difficulty: easy
// add the child first
const childNode = {
  id: "nodeId"
  parentId: "parentId",
  childIds: [...],
  content: "..."
}
tree.set(nodeId, childNode)

// add the child id to the parent node
const parentNode = tree.get(parentId)
parentNode.childIds.push(childNode.id)

// set the parent back to your tree
tree.set(parentNode.id, parentNode)
  • modify or delete a node, regardless of whether it's top-level or child or child of a child
// modify a node. 
// difficulty: easy
const node = tree.get(nodeId)
// ... make the modification
// set it back to the tree
tree.set(nodeId, node)

// delete a node. 
// difficulty: medium
// retrieve the node first
const node = tree.get(nodeId)

// delete it
tree.delete(nodeId)

// delete all of its children
// you need a recursive delete function here to go through all of the node child and child of child and so on
tree.childIds.forEach(recursiveDelete)
// don't forget to delete the nodeId from its parent node. It's easy
...
  • move around a node or an entire subtree, changing its parent or even just its position relative to its siblings
// moving around the tree is quite easy, you just need to follow the `parentId` and `childIds`

// changing a node's parent (same level)
// difficulty: easy
// you just need to change the parentId of the node. And modify the childIds of its old and new parent

// changing a node level, moving its children accordingly
// difficulty: easy
// same as changing a node parent above. Its children will move accordingly

// changing a node level to be a child of one of its children
// difficulty: hard

// get the node
const node = tree.get(nodeId)

// go through its children and update the parentId of each to the node.parentId (moving its children to be the direct child of its parent)
node.childIds.forEach((childId)=> updateParentId(childId, node.parentId))

// set the node parentId to the new one
node.parentId = newParentId

// set new childIds for the node if you want
node.childIds = [...]

// don't forget to set it back on the tree
tree.set(node.id, node)
  • fetch the tree lazily:
// There is no problem at all. You just need to load from the root

Pros and Cons

Pros:

  • Easy implement
  • Easy to get, update content, and delete a node just by its id
  • Easy to move a node along with its children to anywhere you want in the tree

Cons

  • Hard to determine the level of a node (You need to loop through all of its parents)
  • Hard to maintain the constraints of data. Let's say you can't find the parent of a node
  • Hard to tell exactly if a node is a child (child of child...) of another node (You need to loop through all of its parents)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM