Search code examples
statestoresvelte

How to handle related stores with Svelte


I have a store with a list of entities, and another Store with and object that include one of those entities.

I want changes in the first store to be reactively reflected on the second.

I'll provide a quick example with a list of items and a list of invoices

export type Invoice = {
  id: string
  customer: string
  items: InvoiceItem[]
}

export type InvoiceItem = {
  id: string
  name: string
  price: number
}

Whenever the name or price of an invoice item is updated I'd like all the related Invoices to also be updated.

I created this very simple example (repl available here) but in order for the $invoices store to be updated I have to issue a $invoices = $invoices whenever the $items store changes.

Another more elegant way to do it is to subscribe to the items store and from there update the invoices store, like this:

items.subscribe(_ => invoices.update(data => data))
<script>
    import { writable } from 'svelte/store'
    
    let item1 = { id: 'item-01', name: 'Item number 01', price: 100 }
    let item2 = { id: 'item-02', name: 'Item number 02', price: 200 }
    let item3 = { id: 'item-03', name: 'Item number 03', price: 300 }
    let items = writable([item1, item2, item3])
    
    let invoices = writable([
        { id: 'invoice-0', customer: 'customer1', items: [item1, item3] }
    ])
    
    items.subscribe(_ => invoices.update(data => data))     // refresh invoices store whenever an item is changed

    const updateItem1 = () => {
        $items[0].price = $items[0].price + 10
        // $invoices = $invoices    // alternatively, manually tell invoices store that something changed every time I change and item!!!
    }
</script>

<button on:click={updateItem1}>update item 1 price</button>
<hr />
<textarea rows="18">{JSON.stringify($invoices, null, 2)}</textarea>
<textarea rows="18">{JSON.stringify($items, null, 2)}</textarea>

Is this the best way to handle this kind of scenario?


Update: thanks to the great answers and comments I came out with this more complete example: see this repl

I added some functionality that I hope will serve as basis for similar common scenarios

This is how my store api ended up:

// items.js

items.subscribe // read only store
items.reset()
items.upsert(item) // updates the specified item, creates a new one if it doesn't exist


// invoices.js

invoices.subscribe // read only store
invoices.add(invocieId, customer, date) // adds a new invoice
invoices.addLine(invoiceId, itemId, quantity)
invoices.getInvoice(invoice) // get a derived store for that particular invoice


invoice.subscribe // read only store
invoice.addLine(itemId, quantity)

A few highlights

  • invoices now has a lines array, each with an item and a quantity
  • invoices is a derived store that calculate total for each line and for the whole invoice
  • implementes an upsert method in items
  • in order to update invoices whenever an item is modified I run items.subscribe(() => set(_invoices))
  • also created a derived store to get a specific invoice

Solution

  • The solution depends on whether or not you need items independently (one item can be part of multiple invoices) or if it can be part of the invoices. If they can be one big blob, I would create invoices as a store and provide methods to update specific invoices. The items store then would be derived from the invoices.

    // invoices.ts
    const _invoices = writable([]);
    // public API of your invoices store
    export const invoices = {
      subscribe: _invoices.subscribe,
      addItemToInvoice: (invoideId, item) => {...},
      ..
    };
    // derived items:
    const items = derived(invoices, $invoices => flattenAllInvoiceItems($invoice));
    

    However, if they need to be separate - or if it is easier to handle item updates that way -, then I would only store the IDs of the items in the invoice store and create a derived store which uses invoices+items to create the full invoices.

    // items.ts
    const _items = writable([]);
    // public API of your items store
    export const items = {
      subscribe: _items.subscribe,
      update: (item) => {...},
      ...
    };
    // invoices.ts
    import { items } from './items';
    const _invoices = writable([]);
    // public API of your invoices store
    export const invoices = {
      // Assuming you never want the underlying _invoices state avialable publicly
      subscribe: derived([_invoices, items], ([$invoices, $items]) => mergeItemsIntoInvoices($invoices, $items)),
      addItemToInvoice: (invoideId, item) => {...},
      ..
    };
    

    In both cases you can use invoices and items in your Svelte components like you want, interact with a nice public API and the derived stores will ensure everything is synched.