Search code examples
arraystypescriptsvelteconditional-rendering

What's the proper way to render a single element of an array in Svelte?


I have an array of nodes, which in my case are objects that have some data, including an id. I only want to display the active node, which is found by comparing the node ID with the active node ID. I can do that successfully using this:

let active_node_id: string;
active_node_stream.subscribe((value) => (active_node_id = value));

...

{#each nodes as node (node.id)}
  {#if node.id === active_node_id}
    <Node {...node} />
  {/if}
{/each}

However, this pattern of looping through all the nodes only to display 1 node seems wrong. Is there a better way of doing this?


I thought to try to use Svelte's reactive declarations, so as to stop looping through the nodes once the active one was found, like so:

$: active_node_data = nodes.find(n => n.id === active_node_id)

...

<Node {...active_node_data} />

But this gives me the following error (I'm using TypeScript -- it seems to work fine with JavaScript when I try to reproduce it):

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'length')
  at $$self.$$.update

I'm not sure why this error occurs as the active_node_id should always match a node's ID... I have logged the node values and the active_node_id when this error happens and they do indeed match...

Regardless, this approach doesn't feel significantly better than the each with if approach. Is there a better way that's still Svelte-y?


Solution

  • I chatted with my acquaintance yuanchuan about this. He recommended using a Map paired with an if for instances like this, which feels a lot nicer.

    First you need to build a map (I did it where my nodes data was):

    export const node_map = build_data_map(nodes);
    
    function build_data_map(nodes: node[]) {
      const map = new Map<string, node>();
      for (const node of nodes) {
        map.set(node.id, node);
      }
      return map;
    }
    

    Then you can import that map and use it to find the currently active node:

    let active_node_id: string;
    active_node_stream.subscribe((value) => (active_node_id = value));
    
    $: active_node_data = node_map.get(active_node_id);
    
    ...
    
    {#if active_node_data}
      <Node {...active_node_data} />
    {/if}
    

    So using Svelte's reactivity is the way to go, but there are nicer ways to do it like using a map.

    However, the approach provided above prevents transitions when the active node is changed from occurring (in my case I am hiding non-active ones). This is because the above approach uses a single instance of a node, only switching out the data, as opposed to truly transitioning an element in or out. As a result, I stuck with the original approach I had, using the each with if.


    Side note: It turns out the Cannot read properties of undefined (reading 'length') error was coming from inside of my Node component when a property was not being passed in for certain nodes. It was caused by these lines:

    export let options: answer_obj[] = [];
    $: is_choice = options.length !== 0;
    

    I expected options to always have a length (at least be 0) and not be undefined sometimes. $: is_choice = options?.length !== 0; also threw the error, which is surprising. But $: is_choice = options ? options.length !== 0 : false; worked without error. I don't understand why the optional chaining method didn't work (maybe it's an issue with the compiler?) nor do I why this was not an issue with the each approach...