Svelte: Each block with no reactive expressions doesn't update based on length change

Created on 18 Nov 2019  路  10Comments  路  Source: sveltejs/svelte

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

each bug

All 10 comments

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)
Was this page helpful?
0 / 5 - 0 ratings

Related issues

juniorsd picture juniorsd  路  3Comments

clitetailor picture clitetailor  路  3Comments

Rich-Harris picture Rich-Harris  路  3Comments

st-schneider picture st-schneider  路  3Comments

noypiscripter picture noypiscripter  路  3Comments