0.0.2 • Published 3 years ago

svux v0.0.2

Weekly downloads
-
License
ISC
Repository
-
Last release
3 years ago

Svux

Write a Svelte store with Svelte syntax.

Status Experiment :warning:

Why

Write complex store logic with concise & powerful Svelte syntax: reactive variables, and reactive expressions!

Let Svelte track dependencies between your variables and their changes automatically, instead of writing explicit & repetitive chains of derived.

Example

Double.store.svelte

<script>
  import { put } from 'svux'

  // exported props are made writable
  export let x = 0

  // reactive vars are made read-only
  // NOTE this line is optional, shown for illustration
  let double

  // use Svelte's magic to sync results
  $: double = x * 2

  // expose results on the component's store
  $: $put({ x, double })
</script>

App.svelte

<script context="module">
  import { storify } from 'svux'
  import Double from './Double.store.svelte'

  const double = storify(Double)
</script>

<!-- read/write to props -->
<input type="number" bind:this={$double.x} />

<!-- read from exposed variables -->
<pre>{$double.double}</pre>

API

import {
  // --- Consuming ---

  // turns a component into a standalone store
  storify,

  // --- Authoring ---

  // exposes some variables on the component's default store
  put,

  // Lazy / derived stores

  // creates a lazy data source with a lifecycle (start / stop
  // only when subscribed)
  source,
  //
  derive,
} from 'svux'

Consumer

import { storify } from 'svux'

storify

Turns a Svelte component into a standalone store.

import { storify } from 'svux'
import Foo from './Foo.store.svelte'

export const foo = storify(Foo)

The return of storify is a writable store.

An instance of the Foo component will be created when it first becomes active (that is when its subscribers count goes from 0 to 1) to compute and feed values into this store. The component instance will be destroyed when the last subscriber leaves, and a new one will be created if the store becomes active again. The value of the props present (see put bellow) in the store will be preserved and reinjected in all such component instances, though.

This store contents is controlled with put inside the component.

Authoring

import { put, source, derive } from 'svux'

put, source, and derive are "contextual helpers", so you need to use store notation to use them:

$put(...)
$source(...)
$derive(...)

put

Exposes values (variables) on the component's default store (i.e. the store that is produced by storify).

Use $put in a reactive expression to keep the store's value with the state inside the component:

export let a = 0
export let b = 0

$: sum = a + b

$: $put({ a, b, sum })

Once storified, this can then be consumed via the store:

$sumStore.a = 1
$sumStore.b = 1
console.logo($sumStore.sum)

Props (i.e. export let variables) are reinjected in the component when the Svux store is written to.

Writing to variables that are not props / exported is ignored (i.e. simple let variables, including implicit ones created by reactive statements $: foo = ...).

source / derive

One thing you gain when writing a complex store logic with Svelte syntax is implicit variable dependencies tracking: changes are propagated automatically, with no need to explicitly declare dependencies, like you have to do with derived stores for example.

One thing you lose when doing so is fine grained activation of just the stores that are needed, thanks to the subscribe chain created by derived stores. In effect, all the stores in a Svelte component are always subscribed to for the lifetime of the component (as opposed to, say, when they are actually used / accessed). And a live Svelte component is "in tension" at all time: changes are propagated instantaneously, and their side effects updated immediately.

This is not a big problem if you only export one (the default) store. However this becomes annoying if you want to expose multiple semi-independent (or completely independent) stores from the same component, in particular if the source data is "expensive" (e.g. API call, setInterval...). In this case, you want these individual stores to be subscribed as needed, only when actually subscribed by a consumer.

Since this use case is not supported natively in Svelte components, the source / derive pattern gives you a mean to achieve it.

source

First you declare a source, that is a writable store with a lifecycle (start / stop functions) attached to it.

let resolution = 1000

const initialValue = 0
const elapsed = $source(initialValue)

const incrementTime = () => {
  elapsed = elapsed + 1
}

$: elapsed.start = () => {
  const interval = setInterval(incrementTime, resolution)
  // start can return a corresponding stop function
  return () => {
    clearInterval(interval)
  }
}

NOTE The lifecycle is declared in a reactive expression, so Svelte will track and recreate the start function automatically for you when its dependencies change.

Here, we're updating the lifecycle when resolution changes; but we've been careful not to have the source (i.e. $elapsed) itself appear in the reactive expression, to avoid recreating the lifecycle anytime the value of the source changes!

A source store can be used normally in reactive expressions to produce intermediate results:

$: minutes = (elapsed * resolution) / 60

Results can then be exposed for consumption, as well as the dependency on some source(s), with $derive.

derive

$derive produces a readable store that publishes results from inside the component, and that is intended to be exported for external consumption. When this store is subscribed from outside of the component, it will also activate the lifecycle of all the sources it's depending upon.

const elapsed = $source(initialValue)

...

$: seconds = elapsed * resolution
$: minutes = seconds / 60

// declare & export a derived "outlet" store
//
// NOTE repeating the name in the arguments is optional, but it is required
// for SSR support
//
export const time = $derive('time', elapsed)

// sync its value
$: $time = { seconds, minutes }

The depended source stores will be "activated" only when at least one of their derived outlet stores is subscribed.

A source start function will be called when any of its derived outlets is first first subscribed (that is, when the total number of external subscribers --- from outside of the component -- among all depending outlets goes from 0 to 1).

A source start function will also be called anytime the start function itself changes, but ONLY if the source store is currently active.

The stop function optionally returned by a start function will be called when a) the total number of external subscribers to all outlets falls back to 0, or b) before running another (new) start function.