Introducing svelte-entity-store

navillus_dev

Navillus

Posted on July 18, 2021

Introducing svelte-entity-store

Check out the repo for details.

Why?

This is ultimately just a custom store built on top of svelte/store. Like the rest of Svelte, the built in stores are excellent building blocks that aim to give you all the tools you need without trying to solve every single scenario out of the box.

The goal with svelte-entity-store is to provide a simple, generic solution for storing collections of entity objects. Throwing an array of items into a basic writable store doesn't scale well if you have a lot of items and need to quickly find or update one item in the store.

Install

npm i -s svelte-entity-store
Enter fullscreen mode Exit fullscreen mode

Usage

Check out /examples for a working TodoMVC demo based on SvelteKit. More to come!

<script lang="ts">
  import { entityStore } from 'svelte-entity-store'

  // Define your entity interface
  interface TodoItem {
    id: string
    description: string
    completed: boolean
  }

  // Write a getter function that returns the ID of an entity (can be inlined in the constructor also)
  // Currently number and string values are valid IDs
  const getId = (todo: TodoItem) => todo.id

  // Initialize the store
  // (optional) the constructor accepts an Array as a second param
  //            ex: if you rehydrate state from localstorage
  const store = entityStore<TodoItem>(getId)

  // Get a derived store for every active todo
  const activeTodos = store.get((todo) => todo.completed)

  // toggle a todo
  function toggle(id: string) {
    store.update((todo) => ({ ...todo, completed: !todo.completed }), id)
  }

  // clear completed todos
  function clearCompleted() {
    store.remove((todo) => todo.completed)
  }
</script>

{#each $activeTodos as todo (todo.id) }
  // ... render your UI as usual
{/each}
Enter fullscreen mode Exit fullscreen mode

Creating the Store

Creating an instance of the store is pretty straight forward. Svelte has excellent TypeScript support these days, but it isn't a must. Using svelte-entity-store in plain old JavaScript is very similar, just skip the interface definition and <Type> casting.

import { entityStore } from "svelte-entity-store";

// Define your entity interface
interface TodoItem {
  id: string;
  description: string;
  completed: boolean;
}

// Write a getter function that returns the ID of an entity (can be inlined in the constructor also)
// Currently number and string values are valid IDs
const getId = (todo: TodoItem) => todo.id;

// Initialize the store
// (optional) the constructor accepts an Array as a second param
//            ex: if you rehydrate state from localstorage
const store = entityStore<TodoItem>(getId);
Enter fullscreen mode Exit fullscreen mode

Nothing to crazy there so far. For TypeScript we define the model interface, you could also use type if that's your thing. The store also needs to know how to get unique IDs for each item. Right now string and number IDs are supported, but this may be extended later.

Need initial state?

No problem! The store accepts an optional second parameter.

const items = [
  // ... array of TodoItem's to populate the store with
];
const store = entityStore<TodoItem>(getId, items);
Enter fullscreen mode Exit fullscreen mode

Pass in an array of initial items to avoid having to call store.set(items) immediately. This is particularly handy if you are rehydrating the store from cache or localstorage, similar to the TodoMVC example.

Subscribing to entities

The store's get() methods return readable stores to access the entities. The get() method has multiple overrides to serve different uses like grabbing a single entity, filtered list of entities, or everything in the store.

Properly overriding methods in TypeScript was an interesting challenge to get autocorrect and similar tooling to work properly. It's too much to go into here but should turn into a blog post of its own soon.

// Get one entity by ID
const item = store.get("abc-123");

// Get a list of entities by ID
const items = store.get([123, 456, 789]);

// Get a list of entities that match a filter function
const activeItems = store.get((todo) => !todo.completed);

// Or get every entity in the store
const allItems = store.get();
Enter fullscreen mode Exit fullscreen mode

Because the store is returning derived stores, the full power of Svelte's reactivity model just works.

<script lang="ts">
  // Create a computed property based on the get() results
  $: activeItemsCount = $activeItems.count
</script>

// Directly access a single entity
<h1>{$item.title}</h1>

// Or loop over multiple entities
<ul>
  {#each $allItems as item (item.id)}
    <li class:completed={item.completed}>
      {item.title}
    </li>
  {/each}
</ul>
Enter fullscreen mode Exit fullscreen mode

Updating the store

Much like get(), set() has a few different overrides. Calling set will blow away any old entity state, if you need to keep some of the old entities' state check out update() instead (below).

// Replace the existing entity with ID 123, or add it if the ID doesn't exist yet
store.set({
  id: 123,
  description: "Todo #1",
  completed: true,
});

// Or add/replace multiple todos at once
store.set([
  // ... multiple todo objects
]);
Enter fullscreen mode Exit fullscreen mode

Sometimes you just need to change part of an entity without worrying about the entire object. update() solves this, it should look very familiar.

function toggleTodo(todo: TodoItem) {
  return {
    ...todo,
    completed: !todo.completed,
  };
}

// Update a single entity by ID
store.update(toggleTodo, 123);

// In case you already have the entity object and don't want to call getId,
store.update(toggleTodo, todoObj);

// The same goes for lists of IDs or entities
store.update(toggleTodo, [123, 456]);
store.update(toggleTodo, $activeTodos);

// What if you want to only update entities that meet a filter condition?
store.update(toggleTodo, (todo) => todo.completed);

// Go crazy with it and run the update against every entity in the store
store.update(toggleTodo);
Enter fullscreen mode Exit fullscreen mode

Removing entities

Removing will look very similar to update()

// You can remove single entities by ID
store.remove(123)

// By list of IDs
store.remove([123, 456])

// Or remove every item that matches a filter
const isCompleted = (todo) => todo.completed)
store.remove(isCompleted)
Enter fullscreen mode Exit fullscreen mode

Writing the examples here for update and remove, I realized there's no reason that remove shouldn't let you pass in the entity objects similar to update. Time for a github issue.

Fully tested

I went a little overboard trying out a few new (to me) CI and testing tools. On the plus side, the v1.0.0 of store has 100% test coverage and a working TodoMVC example.

All testing is done with lukeed's excellent uvu test framework. I've mostly reached for Jest the last few years but I don't think I'll be turning back. uvu was simple to setup and it really does fly compared to other testing frameworks.

Take a peek at some of the svelte-entity-store tests. It was particularly interesting figuring out a clean way to store subscriptions, i.e. to make sure subscribers get updated state or that subscribers aren't called if an API call didn't actually change the store at all.

Missing features

I purposely didn't add sorting support for v1.0. It can be done without too much headache...

const allItems = store.get();
$sortedItems = $allItems.sort((a, b) => (a < b ? 1 : -1));
Enter fullscreen mode Exit fullscreen mode

but ideally that's built right into the store itself. There's an issue tracking sorting functionality, the best solution is probably to add an optional sort parameter to all get() overloads.

What else?

What'd I miss? File issues for feature requests!

💖 💪 🙅 🚩
navillus_dev
Navillus

Posted on July 18, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related