Search code examples
sveltesvelte-3

Why is Svelte invalidating a variable that isn't changed?


I have a Svelte component that allows editing a list of strings, with the list coming from the writable store $places. This is displayed in the web UI as a list of tags, one longer than the list of places (so that people can add to the list).

let inputs: string[] = []
$: {
  inputs = [...$places, '']
}

The web code renders as follows:

<label for="place0">Places</label>
{#each inputs as input, idx}
  <input id="place{idx}" name="places" type="text" bind:value={input} />
{/each}

For some reason I can't understand, any attempt to type in these input fields causes Svelte to assume that both inputs and $places have been invalidated. Because it thinks $places has been invalidated, the first code block above gets triggered, essentially overriding whatever the user was trying to type and making the field effectively read-only.

I have verified this behavior looking at the compiled code:

function input_input_handler(each_value, idx) {
  each_value[idx] = this.value;
  $$invalidate(0, inputs), $$invalidate(1, $places);
}

As you can see, the last line marks both inputs and $places as invalidated. I can't see any reason why $places should be invalidated, and it's breaking my component.

Can anyone explain why this is happening and how to prevent or circumvent it? Many thanks....


Solution

  • ...how to prevent or circumvent it

    If $places is already initialized when the component is mounted, no reactive statement would be needed and just let inputs = [...$places, ''] would work

    If $places might not yet be set, a subscription could be used instead of the $: reactive statement REPL

    <script>
        import {places} from './places'
            
        let inputs = []
    
        const unsubPlaces = places.subscribe($places => inputs = [...$places, '']) // unsub in onDestroy()
    </script>
    
    {#each inputs as input, idx}
    <input id="place{idx}" name="places" type="text" bind:value={input} />
    {/each}
    

    or

    <script>
        import {places} from './places'
            
        let inputs = []
        
        $: $places, initInputs()
        
        function initInputs() {
             inputs = [...$places, '']
        }
    </script>
    
    {#each inputs as input, idx}
    <input id="place{idx}" name="places" type="text" bind:value={input} />
    {/each}
    

    Like this all values are stored inside inputs, based on $places but with no 'backward connection'.

    With H.B.'s answer the input values are split and the $places ones are still connected to the store plus the seperate new one stored in inputs. Depends on how you need and want to handle the data.