How to create a store derived from both an array store and its items' stores?

Viewed 286

I'm trying to implement what seems to be quite a common pattern but can't write it easily.

In a nutshell, I need a derived store derived from both an array and its items

Let me provide some context because I really feel I'm missing a concept but can't find anything on Stack Overflow, GitHub issues (this one is interestingly related but unresolved yet) or the broader internet (this article provides a solution that doesn't seem to work, unfortunately).

Pattern I'm trying to implement

What I'm trying to achieve really well fits the example of an invoice:

  • an invoice has multiple invoice items
  • each invoice item has a unit price
  • each invoice item has a quantity
  • each invoice item has a subtotal (unit price × quantity)
  • the invoice has a dynamically computed total price

I need the invoice total price to be reactive and to depend on both:

  • the number of invoice items (I want the total price to update automatically when I add an item)
  • each invoice item subtotal (I want the total price to update automatically when I change an item quantity)

Implementation

️ Here is the full implementation in Svelte REPL: https://svelte.dev/repl/164f13f5d99b46e7a8f4cb9627db2aee?version=3.44.1

The data model:

  • an InvoiceItem has:
    • a static name
    • a static unitPrice
    • a writable store quantity (which defaults to 1)
  • an Invoice has:
    • a writable store items (which defaults to [] and will contain many InvoiceItems)

Reactive attributes / computations:

  • an InvoiceItem has:
    • a subTotal attribute which is a derived store (derived from its quantity attribute)
  • an Invoice has:
    • a total attribute which is a derived store (derived from its items attribute) which returns another derived store (derived from all the InvoiceItems)

I call this derived store which returns another derived store a "metastore" (please feel free to suggest a better name ).

Problem

Consuming the metastore requires to first (auto-)subscribe to the higher-level store (total), then to (auto-)subscribe to the derived store returned by the first subscription.

It results in something like this:

$: invoiceTotalMetaStore = invoice.total
$: invoiceTotal = $invoiceTotalMetaStore    

Which is cumbersome / unnecessarily verbose.

Also, this is a simple data model with a 1-level metastore, but what if another use-case would require more deeply nested data? (a derived store that returns another derived store that returns still another derived store )

Is there something obvious I'm missing to implement this pattern? What would the community recommend?

2 Answers

This is a different solution which preserves the basic structure of the Order/Invoice and InvoiceItem class > REPL Like the solution in my other answer it works without any stores. After removing them from the classes the necessary reactivity is achieved by two modifications

  1. using :bind in the {#each} loop
<!-- Item rows -->
{#each invoice.items as item}
  <InvoiceRow bind:item />
{/each}
  1. when calling the method .addItem() on the invoice object Svelte's reactivity isn't triggered since it's not an assignment but a modification inside the object reference. This only changes the object state, but not the Component state -> example. The re-render of the displayed values can be triggered by an assignment of the object to itself after the method call
<button on:click={_ => {invoice.addItem(newItemName, newItemUnitPrice); invoice = invoice}}>

I consider stores as a tool for sharing application state that is used by and should be reactively updated in various unrelated components. In your class modulations they aren't necessary for the basic functionality but they add complexity which can be avoided when using the other tools to achieve the needed reactivity (= re-render / update of the displayed values)

(Why) Is it important to have the invoice class in a seperate file?
What if you move the modeling/logic of the Invoice (Order) class inside the main Component? These lines would build the same functionality

    let invoiceItems = []
        
    function addItem(name, unitPrice) {
        invoiceItems =  [...invoiceItems, new InvoiceItem(name, unitPrice)]
    }
    $: invoiceTotal = invoiceItems.reduce((sum, item) => sum + item.subTotal, 0)

if you add bind:item in the Item rows {#each} block

<!-- Item rows -->
    {#each invoiceItems as item}
        <InvoiceRow bind:item />
    {/each}

you can change the quantity in the InvoiceItem class from the writable store to a normal number and remove the derived from subtotal() as well. (With the binding the changes inside the InvoiceRow component to the item object will affect the invoiceItems in the parent automatically)

export default class InvoiceItem {
    constructor(name, unitPrice, quantity) {
        this.name = name
        this.unitPrice = unitPrice
        this.quantity = 1
    }
    
    get subTotal() {
        return this.unitPrice * this.quantity
    }
}

Have a look at this REPL Like this no store is needed at all for the same functionality.

Related