Why I failed to create the "Solid.js's store" for Svelte, and announcing svelte-store-tree v0.3.1
Tags: typescript, svelte, solidjs, statemanagement
Published: 2022-11-08 04:54:22

---
title: "Why I failed to create the \"Solid.js's store\" for Svelte, and announcing svelte-store-tree v0.3.1"
tags: TypeScript, Svelte, Solidjs, StateManagement
published: true
---

Recently I released a new version of [svelte-store-tree](https://www.npmjs.com/package/svelte-store-tree). It's a state management library for Svelte that I started to develop two months ago, and redesigned its API in the latest version. Today, let me introduce the detailed usage of svelte-store-tree, and compare with a similar library: Solid.js's store feature. Then, I'll share you with a problem of Svelte's store. I'm glad if this article helps you review the design of Svelte in the future.

## Summary

- svelte-store-tree is a state management library for managing tree-like nested structures in Svelte.
- I referred a similar library, Solid.js's store, before creating it. But by contrast with Solid.js's store, svelte-store-tree requires selecting the value inside the store with its `choose` function to handle union type values. It's due to the design of Svelte's store that confines "the API to read the current value of the store" and "the API to update the value of the store" into a single store object.

## Introduction to svelte-store-tree

As the name shows, svelte-store-tree adds[^implementation] methods to [the store objects of Svelte](https://svelte.dev/tutorial/writable-stores), to easily work with trees (nested structures). I developed it for building a complex tree structure intuitively in my current job with Svelte.

[^implementation]: Technically speaking, I implemented svelte-store-tree by rewriting the code of the store after copy-and-pasting it. So it doesn't *add* the methods.

### Example Usage

From this section, I'll use the type below in [the example app you can run here](https://codesandbox.io/p/github/igrep/svelte-store-tree/draft/floral-sound).

```typescript
export type Tree = string | undefined | KeyValue | Tree[];

export type KeyValue = {
  key: string;
  value: string;
};
```

It's really a recursive structure with various possible values.

NOTE: [I uploaded the code of the example app onto GitHub](https://github.com/igrep/svelte-store-tree/tree/main/example), so I'll put the link to the corresponding lines in GitHub after quoting the part of the code.

#### Creating a `WritableTree` Object

To create a `WritableTree` object provided by svelte-store-tree, use the `writableTree` function literally:

```typescript
export const tree = writableTree<Tree>([
  "foo",
  "bar",
  ["baz1", "baz2", "baz3"],
  undefined,
  { key: "some key", value: "some value" },
]);
```

In the same way with the original `writable` function, `writableTree` receives the initial value of the store as its argument. Because `Tree` is a union type, we should specify the type parameter to help type inference in the example above.

#### `subscribe` and `set` like the original store of Svelte

svelte-store-tree complies with [the store contract of Svelte](https://svelte.dev/docs#component-format-script-4-prefix-stores-with-$-to-access-their-values-store-contract). Accordingly prefixing the variable name with a dollar sign `$`, we can `subscribe` `WritableTree`/`ReadableTree`s and `set` `WritableTree`s. Of course two-way binding to the part of the store tree is also available!

```html
<script lang="ts">
  export let tree: WritableTree<Tree>;
  ...
</script>
...
{#if typeof $tree === "string"}
  <li>
    <NodeTypeSelector label="Switch" bind:selected {onSelected} /><input
      type="text"
      bind:value={$tree}
    />
  </li>
...
{/if}
```

[See in GitHub](https://github.com/igrep/svelte-store-tree/blob/30740be58afe4413f0d4e2d454bfe2b340a929e4/example/Tree.svelte#L36-L42)

#### Create a `WritableTree` for a part of the tree by `zoom`

svelte-store-tree provides a way to make a store for some part of the tree as these methods whose name begins with `zoom`:

- `zoom<C>(accessor: Accessor<P, C>): WritableTree<C>`
    - Returns a new `WritableTree` object with an `Accessor` object.
    - An `Accessor` object implements a method to get the child `C` from the parent `P`, and a method to replace the child `C` of the parent `P`.
- `zoomNoSet<C>(readChild: (parent: P) => C | Refuse): ReadableTree<C>`
    - Takes a function to get the child `C` from the parent `P` and then returns a `ReadableTree` object.
    - `ReadableTree` is a store tree that can't `set`. But child store trees `zoom`ed from a `ReadableTree` can `set`. So it is NOT completely read-only.

In the example app, I created `WritableTree`s for writing the `key` and `value` field of the `KeyValue` object, using the `into` function for building `Accessor`s:

```typescript
const key = tree.zoom(into("key"));
const value = tree.zoom(into("value"));
```

However, this isn't correct. svelte-check warns me of type errors detected by TypeScript:

```typescript
/svelte-store-tree/example/Tree.svelte:14:35
Error: Argument of type 'string' is not assignable to parameter of type 'never'. (ts)
...


/svelte-store-tree/example/Tree.svelte:15:37
Error: Argument of type 'string' is not assignable to parameter of type 'never'. (ts)
...
```

The messages are somewhat confusing as it says `parameter of type 'never'`. The type checker means the result of `keyof Tree` is `never` because the type of `tree` is `WritableTree<Tree>`, whose content `Tree` is by definition a union type that can be `undefined`.

To fix the problem, we have to convert the `WritableTree<Tree>` into `WritableTree<KeyValue>` by some means. The `choose` function helps us in such a case.

#### Subscribe the value only fulfilling the condition using `choose`

We can create a store tree that calls the `subscribe`r functions only if the store value matches with the specific condition, by calling `zoom` with an `Accessor` from the `choose` function.

Here I used the `choose` function in the example app:

```typescript
...
const keyValue = tree.zoom(choose(chooseKeyValue));
...
```

`choose` takes a function receiving a store value to return some other value, or `Refuse`. The `chooseKeyValue` function is the argument of `choose` in the case above. It's defined as following:

```typescript
export function chooseKeyValue(tree: Tree): KeyValue | Refuse {
  if (tree === undefined || typeof tree === "string" || tree instanceof Array) {
    return Refuse;
  }
  return tree;
}
```

[See in GitHub](https://github.com/igrep/svelte-store-tree/blob/30740be58afe4413f0d4e2d454bfe2b340a929e4/example/tree.ts#L62-L67)

A `WritableTree` object returned by `choose` calls the `subscribe`r functions when the result of the function (passed to `choose`) is not `Refuse`, a unique symbol dedicated for this purpose[^refuse].

[^refuse]: "zoom", "choose", and "refuse" rhyme.

You might wonder why `choose` doesn't judge by a boolean value or refuse `undefined` instead of the unique symbol. First, the function given to `choose` must specify the return type to make use of [the narrowing feature of TypeScript](https://www.typescriptlang.org/docs/handbook/2/narrowing.html). The [user-defined type guard](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards) doesn't help us with a higher-order function like `choose`. Second, I made the unique symbol `Refuse` to handle objects with nullable properties such as `{ property: T | undefined }` properly.

Let's get back to the problem of converting the `WritableTree<Tree>` into `WritableTree<KeyValue>`. The error by `tree.zoom(into("key"))` is corrected using `choose`:

```typescript
const keyValue = tree.zoom(choose(chooseKeyValue));
const key = keyValue.zoom(into("key"));
const value = keyValue.zoom(into("value"));
```

[See in GitHub](https://github.com/igrep/svelte-store-tree/blob/30740be58afe4413f0d4e2d454bfe2b340a929e4/example/Tree.svelte#L13-L15)

Finally, we can two-way bind the `key` and `value` returned by `zoom`:

```html
<dl>
  <dt><input type="text" bind:value={$key} /></dt>
  <dd><input type="text" bind:value={$value} /></dd>
</dl>
```

[See in GitHub](https://github.com/igrep/svelte-store-tree/blob/30740be58afe4413f0d4e2d454bfe2b340a929e4/example/Tree.svelte#L68-L71)

#### Parents can react to updates by their children

All examples so far doesn't actually need svelte-store-tree. Just managing the state in each component would suffice instead of managing the state as a single large structure. Needless to say, svelte-store-tree does more: it can **propagate updates in the children only to their direct parents**.

For example, assume that the `<input>` bound with the `key` from the last example got new input.

```html
<dl>
      <!--------- THIS <input> ------------->
  <dt><input type="text" bind:value={$key} /></dt>
  <dd><input type="text" bind:value={$value} /></dd>
</dl>
```

Then, the value `set` to `key` is conveyed to the functions `subscribe`ing the `key` itself, or `key`'s direct ancestors including `keyValue`. Functions `subscribe`ing sibling stores such as `value` or the parents' siblings, don't see the update.

To illustrate, imagine the tree were shaped as follows:

![Tree whose root is List 1, and the children are List 2, string and KeyValue](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/265na2gif5ivtcxxv28z.png "Example tree to update one of its children")

When updating the Key, the update is propagated to the three stores: List 1, KeyValue, and Key.

![Update the tree whose root is List 1, and the children are List 2, string and KeyValue](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7uddrpfdctvt75ow7f4d.png "Where the update is propagated when updating the Key")

The Value, string, List 2, and List 2's children don't know the update. Thus, Svelte can avoid extra rerenderings.

To demonstrate that feature, the example app sums up the number of the nodes in the tree by their type:

```html
<script lang="ts">
  import TreeComponent from "./Tree.svelte";
  import { tree, type NodeType, type Tree } from "./tree";

  $: stats = countByNode($tree);
  function countByNode(node: Tree): Map<NodeType, number> {
    ...
  }
</script>

<table>
  <tr>
    <th>Node Type</th>
    <th>Count</th>
  </tr>
    {#each Array.from(stats) as [nodeType, count]}
  <tr>
      <td>{nodeType}</td>
      <td>{count}</td>
  </tr>
    {/each}
</table>
...
```

[See in GitHub](https://github.com/igrep/svelte-store-tree/blob/30740be58afe4413f0d4e2d454bfe2b340a929e4/example/Root.svelte)

## V.S. Solid.js's Store

As I wrote in the beginning of this article, I referred [Solid.js's store](https://www.solidjs.com/tutorial/stores_createstore) feature in developing svelte-store-tree v0.3.1 because it's made for the similar goal with svelte-store-tree. From now on, let me explain in what way svelte-store-tree is similar to Solid.js's store, and what feature of Solid.js's store it failed to support because of the limitation of Svelte's store, respectively. I hope this would be a hint for discussion on the future design of Svelte.

### Quick introduction to Solid.js's Store

Solid.js's store, as the page I referred in the last section described as "Solid's answer to nested reactivity", enables us to update a part of nested structures and track the part of the state. The `createStore` function returns a pair of the current store value, and the function to update the store value (slightly similar to React's `useState`):

ℹ️ All examples in this section are quoted from [the official web site](https://www.solidjs.com/docs/latest/api#updating-stores).

```javascript
const [state, setState] = createStore({
  todos: [
    { task: 'Finish work', completed: false },
    { task: 'Go grocery shopping', completed: false },
    { task: 'Make dinner', completed: false },
  ]
});
```

The first thing of the pair, `state` is wrapped by a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) so that the runtime can track access to the value of the store including its properties.

Using the second value of the pair `setState`, we can specify "path" objects, such as names of the properties and predicate functions to decide which elements to update (if the value is an array), to tell which part of the store value should be updated. For example:

```javascript
// Set true to the `completed` property of the 0th and 2nd elements in the `todos` property.
setState('todos', [0, 2], 'completed', true);

// Append `'!'` to the `task` property of `todos` elements whose `completed` property is `true`.
setState('todos', todo => todo.completed, 'task', t => t + '!');
```

The `setState` is so powerful that it can update arbitrary depth of the nested structure without any other library functions.

In addition, as introduced before, Solid.js's store is designed for separately using the `Proxy`-wrapped value and the function to update, while Svelte's store is designed for using as a single object with `set` and `subscribe` to make it available for two-way binding.

### Feature that affected svelte-store-tree

Somewhat affected by Solid.js's store, I improved the `Accessor` API of svelte-store-tree, used to show how to get deeper into the nested structure. Specifically, `svelte-store-tree` can now compose `Accessor` objects by their `and` method as the `setState` function of Solid.js (the second value of the `createStore`'s result) can compose the "path" objects given as its arguments.

For example, by combining the `into` and `isPresent` `Accessor`, we can make a store that calls `subscribe`r functions if its `foo` property is not `undefined`:

```typescript
store.zoom(into('foo').and(isPresent()));
```

Thanks to them, the code to obtain the `key` property from the `tree` in the example app can be rewritten like below:

```typescript
const key = tree.zoom(choose(chooseKeyValue).and(into("key")));
```

Why don't I define the `zoom` method to receive multiple `Accessor`s to compose? One of the reasons is to simplify the implementation of `zoom`, and another is that it's not a good idea to pile up many `Accessor`s at once to access to too deep part of the nested structure, in my opinion.

In detail, the former means that the code of `zoom` is more concise because it doesn't have to take care of multiple `Accessor`s. Composing `Accessor`s is just the business of the `and` method.

And the latter is a design issue. Diving several levels down into a nested structure at a time is like surgery for the internal: it incurs a risk that the code gets vulnerable to change. Besides, composing `Accessor`s by listing them in the arguments can't work well for recursively nested structures, which I suppose to be a primary use case of svelte-store-tree. Because their depth varies dynamically.

I determined this design based on the usage I assumed, at the sacrifice of verbosity of composing the `and` methods.

### Feature that svelte-store-tree failed to support

I made svelte-store-tree aim for "the Solid.js's store for Svelte". But I couldn't reproduce one of its features anyway: Solid.js does NOT need a feature equivalent to svelte-store-tree's `choose` function[^array]. I wasted a section for describing `choose` in a way!

[^array]: The feature to filter the store value with predicate functions in Solid.js, sounds similar to `choose`, but it's available only when the store value is an array. Note that it's not for narrowing types as `choose` does.

Why doesn't Solid.js's store require a feature like `choose`? That is related to its design that the object to read the store value and the function to update the store value are separated.

Solid.js's store can select the value only by branching in the component on its value using [`Show`](https://www.solidjs.com/tutorial/flow_show) or [`Switch` / `Match`](https://www.solidjs.com/tutorial/flow_switch) (`if` statements in Solid.js).

The same seems appied to svelte-store-tree, but that isn't true. Recall the first example of `choose`:

```html
<script lang="ts">
  // ...
  const keyValue = tree.zoom(choose(chooseKeyValue));
  const key = keyValue.zoom(into("key"));
  const value = keyValue.zoom(into("value"));
  // ...
</script>

{#if typeof $tree === "string"}
  ...
{:else if $tree === undefined}
  ...
{:else if $tree instanceof Array}
  ...
{:else}
  ...
  <dt><input type="text" bind:value={$key} /></dt>
  <dd><input type="text" bind:value={$value} /></dd>
  ...
{/if}
```

The two stores `key` and `value` are used only when the value of `tree` is neither `string`, `undefined`, nor `Array`, but `KeyValue`, as it's chosen by the `{#if} ... {/if}`. Getting `key` and `value` via `choose(chooseKeyValue)` is actually repeating the narrowing-down by the `{#if} ... {/if}`.

So you might try modify the component like following with `{@const ...}` after `key` and `value` are used only in the `{:else}`:

```html
{:else}
  ...
  {@const key = tree.zoom(into("key"))}
  {@const value = tree.zoom(into("value"))}
  <dt><input type="text" bind:value={$key} /></dt>
  <dd><input type="text" bind:value={$value} /></dd>
  ...
{/if}
```

Though svelte-check raises an error about the `{@const ...}` below:

```
/svelte-store-tree/example/Tree.svelte:69:42
Error: Stores must be declared at the top level of the component (this may change in a future version of Svelte) (svelte)
```

It says that stores must be declared at the top-level[^script], thus you can't define a new store in a `{@const ...}` to `subscribe`. It's impossible to make a store only on a specific condition so far.

[^script]: The top-level here seems to mean the top in the `<script>` block of the component.

To avoid that error forcibly, you have the option to split out a component containing only `<input type="text" bind:value={$key} />` and `<input type="text" bind:value={$value} />` then make the component take a `WritableTree<KeyValue>`. However, there is still a type error: the branching beginning with `{#if typeof $tree === "string"}` narrows only `$tree`, that is the current value of `tree`, so doesn't narrow the `tree` to `WritableTree<KeyValue>` from `WritableTree<Tree>`. TypeScript doesn't take that into consideration.

Due to the problem I showed here, I gave up adding the feature following the exisiting convention of the store of Svelte.

Why do we have to narrow the store itself as well as its value in Svelte? Because Svelte assigns both the API to read the store value (`subscribe`) and the API to update (`set` etc.) to the same single object. I'll simplify `WritableTree` to explain the detail:

```typescript
WritableTree<T> = {
  // Get the current value of the store.
  // I replaced the `subscribe` method with this for simplicity.
  get: () => T;

  // Set a new value of the store. This is same as the original `set`.
  set: (newValue: T) => void;
};
```

Let's give a concrete type such as `number | undefined` to `WritableTree` to instantiate a store whose value can contain `undefined`:

```typescript
WritableTree<number | undefined> = {
  get: () => number | undefined;
  set: (newValue: number | undefined) => void;
};
```

Now, let's say we want to remove the `undefined` to convert it to `WritableTree<number>`. Code to use the `get` method of `WritableTree<number>` doesn't expect `get` to return `undefined`, consequently we have to make it return only `number`. Meanwhile code to write a `number` on `WritableTree<number>` just passes `number`s to the `set` method, then we can reuse the function `(newValue: number | undefined) => void` as is[^covariant].

[^covariant]: This difference is widely known as "covariant and contravariant". The Japanese book ["Introduction to TypeScript for Those Who Wants to Be a Pro" (プロを目指す人のためのTypeScript入門)](https://gihyo.jp/book/2022/978-4-297-12747-3) also explains.

Hence, what we have to narrow is only the API to read the store value in fact. While Solid.js's store just has to narrow the current store value by branching, svelte-store-tree has to narrow down both the functions because it forces a single object to contain both the method to read and the method to set.