I'm trying to bind elements in {#each}
block and removing them by click.
<script>
const foodList = [
{ icon: '🍲', elem: null, },
{ icon: '🥫', elem: null, },
{ icon: '🍔', elem: null, },
];
const remove = (index) => {
foodList.splice(index, 1);
foodList = foodList;
};
</script>
{#each foodList as {icon, elem}, index}
<div
bind:this={elems[index]}
on:click={remove}
>
{icon}
</div>
{/each}
In my code i got 2 problems:
{#each}
makes twice more iterations than he should doWhy it works like this?
I'm writing this not for asking help, but for people, that will meet same problems
Both of these problems have the same origin, so I gather them here:
{#each}
block works twice more than expected{#each}
block binding throw an errorAll code tested in Svelte compiler version 3.59.1 & 4.0.5
What do I mean?
Just let me show you, look at code below, how many iterations {#each}
will do?
<script>
const foodList = [
{ icon: '🍲' },
{ icon: '🥫' },
{ icon: '🍔' }
];
</script>
{#each foodList as {icon}}
<div> {icon} </div>
{/each}
If your answer is - 3, then congrats, you are right.
Ok, now we need to bind elements, that we are rendering in {#each}
block.
It's the same code, just added prop 'elem' to objects for each div binding inside {#each}
Look below and try again, how many iterations will make {#each}
?
<script>
const foodList = [
{ icon: '🍲', elem: null, },
{ icon: '🥫', elem: null, },
{ icon: '🍔', elem: null, }
];
</script>
{#each foodList as {icon, elem} }
<div
bind:this={elem}
>
{icon}
</div>
{/each}
Right... we got 6 iterations, twice more.
You can see it by adding some console.log()
at first {#each}
block code, like this:
{#each foodList as {icon, elem}, index}
{console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elem}
>
{icon}
</div>
{/each}
It happens because we used same array for {#each}
iterations and binding.
If we will create new array for binding - the problem will be gone:
<script>
const foodList = [
{ icon: '🍲' },
{ icon: '🥫' },
{ icon: '🍔' }
];
const elems = [];
</script>
{#each foodList as {icon}, index }
{ console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elems[index]}
>
{icon}
</div>
{/each}
Yeah... now the problem is gone and we got 3 iterations, as we expected.
It's a bug that lives for a long time, as I tried to find out - at least 1 year. This leads to different problems in different circumstances as out code become more complex.
(Something like: 'Cannot set properties of undefined')
What do I mean?
Same example, just added removing array item on it's element click:
<script>
const foodList = [
{ icon: '🍲', elem: null, },
{ icon: '🥫', elem: null, },
{ icon: '🍔', elem: null, }
];
const remove = (index) => {
foodList.splice(index, 1);
foodList = foodList;
};
</script>
{#each foodList as {icon, elem}, index}
{console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elem}
on:click={remove}
>
{icon}
</div>
{/each}
We expect that if we make a click on div
- array item, to which it was bound will be sliced and we will lose them on screen. Correct... but we got error, because {#each}
block make still 3 iterations, not 2 as we were waiting for.
Again for clearness, our code steps: foodList length is 3 -> we make a click on food icon -> foodList length is 2 (we cut item with clicked icon).
After this {#each}
should do 2 iterations to render each left icon in foodList, but he did 3!
That's why we have the problem, our code trying to write new property to undefined (item is sliced so when we are trying to read\write it - there is undefined.
// foodList [ { icon: '🍲', elem: null, }, { icon: '🥫', elem: null, }]
foodList[2].elem = <div>; // "Cannot set properties of undefined"
It's a bug and it happens if we used same array for {#each}
iterations and binding.
The most clean fix on my question is to separate iterable and binding data into different arrays:
<script>
const foodList = [
{ icon: '🍲' },
{ icon: '🥫' },
{ icon: '🍔' }
];
let elems = [];
const remove = (index) => {
foodList.splice(index, 1);
foodList = foodList;
};
</script>
{#each foodList as {icon, cmp}, index}
{console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elems[index]}
on:click={remove}
>
{icon}
</div>
{/each}
But... Let's look inside out new elems
array by adding $: console.log(elems);
(it's reactive expression, that will print elems
array each time as it changes)
<script>
const foodList = [
{ icon: '🍲' },
{ icon: '🥫' },
{ icon: '🍔' }
];
let elems = [];
const remove = (index) => {
foodList.splice(index, 1);
foodList = foodList;
};
$: console.log(elems);
</script>
{#each foodList as {icon, cmp}, index}
{console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elems[index]}
on:click={remove}
>
{icon}
</div>
{/each}
Looks like a have 2 news for you
null
item in elems
arrayIt means that problem is still here( {#each}
block makes still 1 extra iteration for sliced item).
For now we can filter elems
array after foodList slicing, just do it after page update, such as tick()
.
Full code:
<script>
import { tick } from 'svelte';
const foodList = [
{ icon: '🍲', elem: null, },
{ icon: '🥫', elem: null, },
{ icon: '🍔', elem: null, }
];
let elems = [];
const remove = async (index) => {
foodList.splice(index, 1);
foodList = foodList;
await tick();
elems = elems.filter((elem) => (elem !== null));
};
$: console.log(elems);
</script>
{#each foodList as {icon, elem}, index}
{console.log('each iteration: ', index, icon) ? '' : ''}
<div
bind:this={elems[index]}
on:click={remove}
>
{icon}
</div>
{/each}
Keep in mind: {#each}
block still works 1 extra time and we got null as bound elem, we just filtered it after the page updates.
Don't know what to say for real... I wasted too much time on this **** trying to figure out why my code isn't work as it should be.
I like svelte, but I don't like bugs
I really hope this little guide will helps some of you to save a lot of time.
Will be glad to your corrections, see you and don't let 🐞 win.
Yep, it takes time, but... Never know when you will needs help, share your knowledge