Updating Tree-like Component in Svelte
Updated September 6, 2023
Tree-like components are notoriously difficult to create, especially if you also want to implement additional features like adding, removing, reordering, accessibility, etc. It can be the most challenging component in your entire app. In Svelte, even simply updating tree data is not trivial at all because you need to be aware of the cons in each method of updating.
In this article, I will share some tips for updating tree components in Svelte.
Rendering
This is how you do recursive rendering in Svelte.
I’m creating a helper utility called h
to make tree items more manageable. As you can see, always start with the root (a single tree item that holds all the children). Doing it without a root is usually a bit more complicated when you do advanced operations later. When creating a tree, I always include a parent
property, which is super helpful later if you want to do something complex.
Also, include an id
property. This is very important if you want your tree to be modifiable (e.g., adding, removing, and reordering items). Without an id
, expect your tree to break, with or without an error (the worst-case scenario) when you perform those operations. For the sake of simplicity, I just use Math.random()
, but you can use a package like uid
.
The Problems with Updating
You can click on the arrow to trigger the folding of the children, so reactivity seems to be working. But what if you want to trigger it from outside of Item.svelte
?
Try setting the fold for the src
item from App.svelte
!
You might do something like this:
tree.children[1].fold = false
Which does the job. But what if the item is deeply nested? Of course, you don’t want to code something like this:
tree.children[1].children[1].children[5].fold = false // Yikes!
Also, keep in mind that if you store the children into a variable first, your reactivity will not work because of how compiled Svelte code works.
let item = tree.children[0]
item.fold = false // It won't trigger reactivity
Now, you can clearly see that this problem is not something trivial. Because if you want a modifiable tree, in the future, you might implement features like:
// Adding
item.appendChild(someNewItem)
// Removing
item.remove()
If you can’t store a deeply nested item inside a variable, you will not get this kind of beautiful API.
That’s sad. So, you might think, why not just store every item as writable
stores? You can, but you will hit a roadblock:
ValidationError: Stores must be declared at the top level of the component (this may change in a future version of Svelte)
This is what happens if you store a writable
inside a writable
(nested writable
) and try to access it outside the top-level function, which is most likely what you will do if you build a full app.
So, what to do?
Solution
Object mutation.
If you mutate every tree item to have a set
function, it will work.
<!-- file: Item.svelte -->
<script>
export let data
// Mutating passed data. Because of how compiled Svelte code works,
// calling this will trigger reactivity
data.set = (newData) => {
data = Object.assign(data,newData)
}
</script>
...
“Whoa!? I heard mutating an object directly in a case like this is a bad practice, no?”
Everything has a trade-off. We need to be wise when choosing what to use. But I can see where you’re coming from, especially if you’ve used React. I don’t think this is a bad practice at all, considering the other alternatives, which will lead to even more complicated and less straightforward code. This method also has the advantage of only triggering the part of the tree that needs changing, compared to setting the value from the tree
variable, which will rebuild the entire tree.
I personally use this method in my app (a web editor), and it works very nicely.
Now you can control tree items outside of Item.svelte
.
<!-- file: App.svelte -->
<script>
setTimeout(() => {
let item = tree.children[1]
item.set({ fold: false })
}, 2000)
</script>
...