'Svelte Performance: Derived from custom stores of arrays

I'm making a Trello clone in Svelte using custom and derived stores. Svelte reactivity watches for assignment, so I need to reassign my array stores, but I noticed stores derived from writeable arrays recalculate the entire array. Surely I can be more performant here, especially if only a single array value is updated?

// The store in question, edited for terseness
const cards = writeable([]);

// adding to the card somewhere in code
cards.update((cards) => (cards = [...cards, card]));

// the derived store, that loops through all cards (because of reduce)
// this groups the card by its listId
const cardsGroupedByList = derived(cards, ($cards) => {
  return $cards.reduce((list, card) => {
    console.log('recalculating cards');
    const data = list.get(card.listId) || [];
    return list.set(card.listId, [...data, card]);
  }, new Map());
});

In the console, cardsGroupedByList will print n times, but Svelte only renders the corresponding component once, so this is purely data manipulation. Is there a better way to do this?

Here's a working Svelte REPL with multiple lists and cards.



Solution 1:[1]

One potential solution is to have 2 custom stores. It only updates once, but definitely loses the elegance of a derived store.

// cards now need their set and add methods modified
// otherwise I risk my group running out of sync
export const cards = (() => {
  const { subscribe, set, update } = writable<Card[]>([]);

  return {
    subscribe,
    set: (cards: Card[]) => {
      cardsGroupedByList.set(cards); // set the group, reduce once
      set(cards);
    },
    add: (card: Card) => {
      cardsGroupedByList.update(card); // add to the group
      update((cards) => (cards = [...cards, card]));
    },
  };
})();

// groupBy becomes a custom store that retains internal state
// run the risk of the group becoming out of sync with cards
// could have issues when the card moves -- would need to add/remove or recalc
export const cardsGroupedByList = (() => {
  const init = new Map();
  const { subscribe, set, update } = writable(init);

  function groupBy(groups, card) {
    console.log('calculating groups');  // same console log, only happens once
    const data = groups.get(card.listId) || [];
    return groups.set(card.listId, [...data, card]);
  }

  return {
    subscribe,
    set: (cards: Card[]) => set(cards.reduce(groupBy, init)),
    update: (card) => {
      update((groups) => groupBy(groups, card));
    },
  };
})();

Solution 2:[2]

In your specific use case you dont need to create an additionnal store and you could simply reduce in the App script :)

App.svelte

<script>
  import { cards } from './card-store.js';
  import Card from './Card.svelte';
  import CardForm from './CardForm.svelte';
  let cardsList
  $: cardsList = $cards.reduce((acc, card) => {
    acc[card.listId] = acc[card.listId] || []
    acc[card.listId].push(card)
    return acc
  }, {})
</script>

{#each [1,2,3] as idx}
  <h2>List: {idx}</h2>
  <ul>
    {#each cardsList[idx] as card}
      <Card {...card} />
    {/each}
  </ul>
  <CardForm listId={idx} />
{/each}

And remove the cardsGroupedByList entry from the card-store

edit: Im not sure this is much different from what you do since the structure is recomputed each time anyway :/

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 bholtbholt
Solution 2 Ji aSH