Describe the bug
The content of an #each block that is only dependent on the number of items in the array being iterated doesn't update when the array length increases.
Bug was introduced in 3.6.0 (Works in 5.4.0)
Logs
Please include browser console and server logs around the time this bug occurred.
To Reproduce
https://svelte.dev/repl/3191259e0c3948e3b0f2875623709aab?version=3.14.1
click the button
expected: the number of "val"'s on the screen increases,
actual: no change
This isn't a bug. Svelte's reactivity is triggered by assignment, so it won't re-render on mutations like array pushing. Checking for mutations would most likely be an unnecessarily hard and performance heavy task. Restricting the reactivity to assignments helps keep Svelte fast.
Updating arrays is described in the Svelte tutorial, https://svelte.dev/tutorial/updating-arrays-and-objects.
thanks @pettersmoen, but there is an assignment arr = arr in the handler
Oh, sorry, @halfnelson, I didn't spot that. My bad :sweat_smile:
The bug disappears in two cases that I've found:
I'm not sure arr = arr is actually an assignment, as it doesn't change the object being referenced. If svelte is checking by using Object.is(obj1, obj2); then Object.is([],[]); is false (so would update) and
var arr = [];
Object.is(arr, arr= arr);
Is true, so it wouldn't update.
You'd be better off with your onclick being arr = [...arr, arr.length+1]; Which is what the tutorial says. Though it weirdly implies push with = would work.
Taking your example and creating a new array however works. So I'm not sure this is a bug so much as it is the tutorial being misguiding.
<script>
let arr = [1,2,3,4];
</script>
{#each arr as val}
<div>val</div>
{/each}
<button on:click={() => { arr = [...arr, arr.length+1] }}>Add</button>
No, this is a common Svelte idiom for manually invalidating something that might have been mutated without there being a visible = sign somewhere. We want arr.push(foo); arr = arr; to work, and this is a bug. If you look at the generated code in both cases, you can see the $$invalidate() call that is added, but for some reason this isn't triggering the appropriate updates.
In that case I believe the problem is with the safe_not_equal and not_equal functions in the internal utils.
a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function')
Specifically a !== b doesn't return the result necessary when used on an array. E.G.
'test' !== 'test'; returns false
[] !== []; returns true
let arr = [];
arr !== arr; // returns false
The problem is that the generated code always compares the length of the array to itself, which never changes.
Here's what is generated (abbreviated):
if (dirty & /*arr*/ 1) {
const old_length = each_value.length;
each_value = /*arr*/ ctx[0];
let i;
for (i = old_length; i < each_value.length; i += 1) {
// updating stuff
}
Changing the old_length line to:
const old_length = each_blocks.length;
Fixes it... I'll see if I can get this in a PR before I'm done today.
Unfortunately, I broke another test doing said change so PR is forthcoming...
fwiw, if you make a shallow copy, it works:
let _arr = [...arr]
_arr.push(arr.length+1);
arr = _arr
and of course if you assign with concat, works:
arr = arr.concat(arr.length+1)