Skip to content
This repository has been archived by the owner on Aug 28, 2020. It is now read-only.

Latest commit

 

History

History
317 lines (218 loc) · 12.4 KB

stores.md

File metadata and controls

317 lines (218 loc) · 12.4 KB

Svelte Store Recipes

Table of Contents

Stores

This guide assumes you understand the basics of Svelte Stores. If you aren't familiar with them then working through the relevant tutorial and reading the store documentation are highly recommended.

Svelte stores offer a simple mechanism to handle shared state in your Svelte application but looking beyond the built-in store implementations will unlock a whole world of power that you could never have dreamed of. In this episode of The Tinest Kitchen we'll take a close look at The Store Contract, learn how to implement Custom Stores, by making use of the built-in store API, and explore how we can implement a completely custom store without using the built-in stores at all.

The store contract

The built-in Svelte stores (readable, writable, and derived) are just store implementations and while they are perfectly capable of handling many tasks, sometimes you need something more specific. Although often overlooked, the store contract is what gives these stores their power and flexibility. Without this contract, svelte stores would be awkward to use and require significant amounts of boilerplate.

Svelte does not compile your javascript files and, as such, only observes the store contract inside Svelte components.

store.subscribe

At its simplest, the store contract is this: any time Svelte sees a variable prepended with $ in a Svelte component (such as $store) it calls the subscribe method of that variable. The subscribe method must take a single argument, which is a function, and it must return a function that allows any subscribers to unsubscribe when necessary. Whenever the callback function is called, it must be passed the current store value as an argument. The callback passed to subscribe should be called when subscribing and anytime the store value changes.

The following examples aren't the exact code that Svelte produces, rather, simplified examples to illustrate the behaviour.

This:

import { my_store } from "./store.js";

console.log($my_store);

Becomes something like this:

import { my_store } from "./store.js";

let $my_store;
const unsubscribe = my_store.subscribe((value) => ($my_store = value));
onDestroy(unsubscribe);

console.log($my_store);

The callback function passed to my_store.subscribe is called immediately and whenever the store value changes. Here, Svelte has automatically produced some code to assign the my_store value to $my_store whenever it is called. If $my_store is referenced in the component, it also causes those parts of the component to update when the store value changes. When the component is destroyed, Svelte calls the unsubscribe function returned from my_store.subscribe.

store.set

Optionally, a store can have a set method. Whenever there is an assignment to a variable prepended with $ in a Svelte component it calls the set method of that variable with newly mutated or reassigned $variable as an argument. Typically, this set argument should update the store value and call all subscribers, but this is not required. For example, Svelte's tweened and spring stores do not immediately update their values but rather schedule updates on every frame for as long as the animation lasts. If you decide to take this approach with set, we advise not binding to these stores as the behaviour could be unpredictable.

This:

$my_store = "Hello";

Will become something like:

$my_store = "Hello";
my_store.set($my_store);

The same is true when assigning to a nested property of a store.

This:

$my_store.greeting = "Hello";

Becomes:

$my_store.greeting = "Hello";
my_store.set($my_store);

Although Svelte's built-in stores also have an update method, this is not part of the contract and is not required to benefit from the automatic subscriptions, unsubscriptions, and updates that the store contract provides. Stores can have as many additional methods as you like, allowing you to build powerful abstractions that take advantage of the automatic reactivity and cleanup that the store contract provides.

To summarise, the store contract states that svelte stores must be an object containing the following methods:

  • subscribe - Automatically called whenever svelte sees a $ prepended variable (like $store) ensuring that the $ prepended value always has the current store value. Subscribe must accept a function which is called both immediately, and whenever the store value changes, it must return an unsubscribe function. The callback function must be passed the current store value as an argument whenever it is called.
  • set - Automatically called whenever Svelte sees an assignment to a $ prepended variable (like $store = 'value'). This should generally update the store value and call all subscribers.

Custom stores

Now we know what Svelte needs to make use of the shorthand store syntax, we can get to work implementing a custom store by augmenting a svelte store and re-exporting it. Since Svelte doesn't care about additional methods being present on store objects, we are free to add whatever we like as long as subscribe, and optionally set, are present.

Linked stores

In this first example, we are creating a function that returns two linked stores that update when their partner changes, this example uses this linked store to convert temperatures from Celsius to Fahrenheit and vice-versa. The interface looks like this:

store : { subscribe, set }
function(a_to_b_function, b_to_a_function): [store, store]

To implement this store, we need to create two writable stores, write custom set methods for each, and return an array of store objects containing this set method.

We define a function first as this implementation is a store creator allowing us plenty of flexibility. The function needs to take two parameters, each being a callback function which is called when the stores are updated. The first function takes the first store value and returns a value that sets the value of the second store. The second argument does the opposite. One of these functions is called when the relevant set method is called.

import { writable } from "svelte/store";

function synced(a_to_b, b_to_a) {
  const a = writable();
  const b = writable();
}

The set methods call their own set with the provided value and call the partner store's set when the provided value is passed through the callback function.

// called when store_a.set is called or its binding reruns
function a_set($a) {
  a.set($a);
  b.set(a_to_b($a));
}

// called when store_b.set is called or its binding reruns
function b_set($b) {
  a.set(b_to_a($b));
  b.set($b);
}

All we need to do now is return an array of objects each containing the correct subscribe and set method:

return [
  { subscribe: a.subscribe, set: a_set },
  { subscribe: b.subscribe, set: b_set },
];

Inside a component, we can use this synced store creator by deconstructing the returned array. This ensures Svelte can subscribe to each store individually, as stores definitions need to be at the top level for this to happen. This store can be imported and reused in any component.

import { synced } from "./synced.js";

const [a, a_plus_five] = synced(
  (a) => a + 5,
  (b) => a - 5
);

$a = 0; // set an initial value

Since we have written custom set methods, we are also free to bind to each individual store. When one store updates, the other also updates after the provided function is applied to the value.

See it in action below. The following example uses the synced store to convert between Celsius and Fahrenheit in both directions.

<script>
  import { synced } from "./linkable";

  export let initialCelsius = null;
  export let initialFahrenheit = null;

  const [C, F] = synced(
    (C) => (C * 9) / 5 + 32,
    (F) => ((F - 32) * 5) / 9
  );

  if (initialCelsius && initialFahrenheit) {
    console.error(
      "You can only set one inital temperature. Please set initialCelsius or initialFahrenheit but not both."
    );
  } else if (initialCelsius) {
    $C = initialCelsius;
  } else if (initialFahrenheit) {
    $F = initialFahrenheit;
  } else {
    $C = 0;
  }
</script>

<input bind:value="{$C}" type="number" /> ºC =
<input bind:value="{$F}" type="number" /> ºF

Play around with it in the REPL.

a custom implementation of the builtin store

A simple store is about 20 lines of code, in many cases the built-in stores provide good primitives you can build on but sometimes it makes sense to write your own.

The most basic implementation would look something like this (REPL) (this is simpler than the built-in stores):

function writable(init) {
  let _val = init;
  const subs = [];

  const subscribe = (cb) => {
    subs.push(cb);
    cb(_val);

    return () => {
      const index = subs.findIndex((fn) => fn === cb);
      subs.splice(index, 1);
    };
  };

  const set = (v) => {
    _val = v;
    subs.forEach((fn) => fn(_val));
  };

  const update = (fn) => set(fn(_val));

  return { subscribe, set, update };
}

From this point you could add whatever functionality you wanted.

Edit: Probably worth mentioning that this is a full writable implementation, only the subscribe method and its return value (an unsubscribe function) are required to be a valid store.

Reactive Context With Stores

While a Svelte store provides a mechanism to access state by multiple unrelated components the Svelte context API provides a way for a parent component to pass down data to any child components. This way data doesn't have to be passed down as props.

However, the getContext and setContext functions can only be called during component initialization which means their contents cannot be updated during runtime.

So if you update a variable pulled from context... it won't update in any Child components that uses it.

//App.svelte
<script context="module">
  export const KEY = {}
</script>

<script>
	import {setContext} from 'svelte';
	import Name from './Name.svelte';

	let name = 'Rick';

	setContext(KEY, name);

	function changeName(){
		name = 'PickleRick'; // Only changes name locally -> Doesn't change name in <Name/>
		console.log(name) // logs 'Pickle Rick'
	}
</script>

<Name />
<button on:click={changeName}>Change Name</button>
//Name.svelte
<script>
	import {getContext} from 'svelte';
  import {KEY} from './App.svelte'

	let name = getContext(KEY);
</script>

<h1> My name is: {name}</h1>

The export const KEY = {} in the context="module" script tag is just a way to avoid namespace collisions when using context by using an object key instead of a string.

You can see the name does indeed update as it's new value is logged. However the context has already been pulled in the Name child component so nothing is reflected on screen.

So how do you use context to pass information down AND have it's content update at runtime? We can simply use a store!

//App.svelte
<script context="module">
  export const KEY = {}
</script>

<script>
	import {setContext} from 'svelte';
	import {writable} from 'svelte/store';

	import Name from './Name.svelte';

	let name = writable('Rick');

	setContext(KEY, name);

	function changeName(){
		$name = 'PickleRick'; // Changes the store -> Changes name in <Name/>
		console.log($name) // logs 'Pickle Rick'
	}
</script>

<Name />
<button on:click={changeName}>Change Name</button>
//Name.svelte
<script>
	import {getContext} from 'svelte';
  import {KEY} from './App.svelte'

	let name = getContext(KEY);
</script>

<h1> My name is: {$name}</h1>
  • Context w/ No Update: REPL
  • Context w/ Update: REPL

Back to Table of Contents