Tony Sullivan

Introducing svelte-entity-store

Navillus.dev

tony @navillus.dev

6 min read

A (work-in-progress) generic entity store for Svelte projects.

A (work-in-progress) generic entity store for Svelte projects.

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

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}

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)

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)

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()

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>

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
])

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)

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)

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))

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!