Search code examples
sveltesvelte-3

Svelte component does not see prop update if in one slot, but does if in another slot


In the following component (call it “annotation screen”), component DocPicker let the user set the variable selectedDoc, which in turn is supposed to update two components: Form and SearchResultView.

<script lang="ts">
  import type { AppScreen, QuestionGouv } from '../../types.js';
  import Screen from '../../Screen.svelte';
  import DocPicker from './DocPicker/DocPicker.svelte';
  import SearchResultView from '../../SearchResultView.svelte';
  import Form from './Form.svelte';

  export let screen: AppScreen;
  
  let selectedDoc: QuestionGouv;
  let isPickerOpen: boolean;
</script>

<Screen bind:screen>
  <svelte:fragment slot="sidebar">
    <DocPicker bind:picked={selectedDoc} bind:isOpen={isPickerOpen}/>
    {#if selectedDoc}
      <Form doc={selectedDoc}/>
    {/if}
  </svelte:fragment>
  <svelte:fragment slot="content">
    {#if selectedDoc}
      <SearchResultView doc={selectedDoc} on:close={() => {selectedDoc = undefined}}/>
    {/if}
  </svelte:fragment>
</Screen>

The problem is that the Form component is not updated when selectedDoc changes. The first time selectedDoc goes from undefined to some value, component Form appears with the proper value, but if later selectedDoc changes (because the user used DocPicker), component Form stays with the previous value.

SearchResultView is updated every time though, works like a charm, so I though the problem is coming from the Form component instead, but if I copy line <Form doc={selectedDoc}/> right next to where SearchResultView is (that is, in the <svelte:fragment slot="content">), this instance of Form works without a problem! Similarly, if I put another instance of SearchResultView in <svelte:fragment slot="sidebar">, this instance of SearchResultView does not update.

So it seems to be related to where the component is inserted, namely, in which slot of the Screen component. But the Screen component is pretty trivial, it boils down to:

<Sidebar>
  <div class="screen-sidebar">
    <slot name="sidebar"></slot>
  </div>
</Sidebar>
<slot name="content"></slot>

And the Sidebar component is trivial as well:

<div class="sidebar">
  <slot></slot>
</div>

Solution

  • Okay, got it. It turned out that the cause of the problem was not in the code snippets that I put in my question. I did something you are probably not supposed to do at all in Svelte in the DocPicker component. The Svelte compiler did not complain but got really confused apparently, and this resulted in the weird behavior I describe in the question.

    Here are the details of the problem and how I found it.

    I tried to come up with a minimal working example of the situation so that I could put it in a Svelte REPL, and of course the problem was not reproducing.

    So I tried many intermediate steps from my problematic situation to the failed minimal working example, to see which precise step would brings back the bug. And what was bringing it back was the bind:isOpen={isPickerOpen} prop binding in the DocPicker component. So the problem is probably linked to this component.

    I had a look in the code of this DocPicker component, and prop isOpen is used in a weird way. I had even left a comment warning about it:

    export let isOpen: boolean = true;
    $: isOpen = !Boolean(picked);
    // XXX is it good practice to "force" a variable that is also reactively assigned?
    function openPicker() {
      isOpen = true;
    }
    

    So guess I now have my answer to the question in the comment: no, it's probably not a good idea. Or maybe not this way. If it's always a bad idea, couldn't the Svelte compiler raise an error when it sees this?

    Anyway, I changed the way isOpen is managed (not through reactive assignments, only imperative ones) and now it works like a charm.