Search code examples
observablereactive-programmingsvelte

How to filter a svelte store using a dynamic filter


I have a derived store which has to filter an entries object using HTML selects for the filter. Now I introduced an extra filter store (observable) to force the derived store callback to run when the filter store changes.

But is it possible to trigger the callback in the derived store below without the filter store when the filter changes? Do I need this extra store here? The below code works fine. I'am curious.

import { writable, derived } from 'svelte/store';
import { entries } from './../stores/entries.js';

export const filter = writable({
  // to update filter use: $filter.kind = ... 
  // or: filter.update(o => Object.assign(o, {kind: .., batchId: ...}));
  batchId: 'all',
  kind: 'all',
});

let list, total;

export const view = derived(
  [filter, entries], 
  ([$filter, $entries], set) => {

    total = 0;

    if ($entries) {
      // filter by HTML selects: kind, batchId
      list = Object.keys($entries.map).sort().reduce((a, key) => {
        if ((['all', $entries.map[key].description.kind].includes($filter.kind))
          && (['all', $entries.map[key].parentId].includes($filter.batchId))) {
            total += $entries.map[key].grossValue;
            a.push($entries.map[key]);
        };
        return a;  
      }, []);
      set({list, total});
    };

    return () => {
      set(null);
    };
  }, null
);

Update: a pseudo derived writeable using a custom store

import { writable, derived } from 'svelte/store';
import { entries } from './../stores/entries.js';

let list, total;

const filter = writable({batchId: 'all', kind: 'all'});

export const view = () => {
  const viewDerived = derived([filter, entries], 
    ([$filter, $entries]) => {

      total = 0;
      if ($entries) {
        // filter by HTML selects: kind, batchId
        list = Object.keys($entries.map).sort().reduce((a, key) => {
          if ((['all', $entries.map[key].description.kind].includes($filter.kind))
            && (['all', $entries.map[key].parentId].includes($filter.batchId))) {
              total += $entries.map[key].grossValue;
              a.push($entries.map[key]);
          };
          return a;  
        }, []);
        return {list, total};
      } else return null;

    }
  );
  // custom store methods
  return {
    subscribe: viewDerived.subscribe,
    set: filter.set,
    update: (obj) => filter.update(o => Object.assign(o, obj)),
    reset: () => filter.set({batchId: 'all', kind: 'all'}),
  };
}();

Solution

  • You could extract the logic from the derived store into a function / object you control yourself, with an API to manually trigger an update... But that would not be a good idea. That would break some encapsulation for no benefit and all.

    Your writable + derived solution is, IMO, the most straightforward and elegant solution. It explicitly outlines data dependencies, and cleanly separate concerns, with no arcane code involved. It also provides Svelte what it needs to monitor changes, and manages subscriptions automatically for you, with the finest granularity.

    This is a nice pattern, and it is perfectly appropriated for your use case. I would keep it like this.