[svelte store][02] svelte store's writable()

let's summarize the writable, subscribe, and unsubscribe provided by the svelte store.

1. Before using svelte store

svlete provides writable() function for state management.

TS
import { writable } from 'svelte/store';

we'll start with a simple example that fetches weather information from the server and renders it to the screen.

the following weather information is provided by the server

JS
{ temperature: "35C", humidity:"65%" time: new Date() }

mimic the function to get the server data as follows (api.js)

api.js
export const api = { weather:{ load(lat, lng) { return new Promise((resolve) => { setTimeout(() => { resolve({ temperature: "20C", humidity: "65%", time: new Date() }) }, 2000) }) } } }
  • we're mimicking a delay of about 2 seconds.

the Weather.svelte component is implemented simply as follows.

Weather.svelte#01
<script import {writable} from 'svelte/store' import {api} from './api' const weather = writable(undefined); api.weather.load().then(data => { $weather = data; }) </script>
Weather.svelte#01
<div> <h5>weather</h5 <h5>weather</h5> {#if $weather} <span>{$weather.temperature}</span> <span>{$weather.humidity}</span> {:else} <span>...loading</span> {/if} </div>

2. What is "$weather"

the object returned by calling the function writable on Weather.svelte is called a store object.

the store object provides reactive functionality that automatically updates the screen when the data changes.

A store is an object that allows reactive access to a value via a simple store contract. The svelte/store module contains minimal store implementations which fulfill this contract.

these store objects can be read or written to by simply prefixing them with a dollar sign ($) only within **_*.svelte files.

In this document, we will refer to the dollar sign as the store prefix.

if we implemented the following code to update data without the store prefix, it would look like this

without store prefix
// using store prefix api.weather.load().then((data) => { $weather = data; }); // without store prefix api.weather.load().then((data) => { weather.set(data); });
  • store.set(...) - For $weather = data to work, the store object must implement the method set.
  • in the view area (HTML), the area referenced by $weather will be updated.

more code is needed to implement the Weather.svelte#01 code to detect state changes without the store prefix.

Weather.svelte#01
<script import {writable} from 'svelte/store' import {api} from './api' const weather = writable(undefined); api.weather.load().then(data => { $weather = data; }) </script> </script <div> {#if $weather} <span>{$weather.temperature}</span> <span>{$weather.humidity}</span> {:else} <span>...loading</span> {/if} </div>

Weather.svelte#02 is the implementation when store prefix is not used.

Weather.svelte#02
<script import {writable} from 'svelte/store' import {api} from './api' const weather = writable(undefined); api.weather.load().then(data => { weather.set(data); }) let _wdata = undefined; weather.subscribe((weatherData) => { _wdata = weatherData }) </script>
Weather.svelte#02
<div> {#if _wdata} <span>{_wdata.temperature}</span> <span>{_wdata.humidity}</span> {:else} <span>...loading</span> {/if} </div>
  • subscribe(callback) - The store object must implement the method subscribe to be reactively updated in the view.

**If you don't use the *store prefix, you must register a callback function with the subscribe method directly, such as Weather.svelte#02, to receive state changes.

you'll also need a variable (_wdata) to store the state in.

SVELTE
let _wdata; // required to get state of store object weather.subscribe((weatherData) => { _wdata = weatherData })
  • in svelte, assigning a value to a variable as above works reactively. _wdata = weatherData link
  • however, it doesn't work reactively if the property inside the object changes. if I change it to _wdata.temperature = "40C", the screen doesn't update.

There is a bug in the code of Weather.svelte#02.

3. subscribe, unsubscribe

let's say the weather information is a global state shared by multiple components.

Weather.svelte
import {writable} from 'svelte/store' import {api} from './api' // Don't create global state inside the component. const weather = writable(undefined); // Don't call the API directly api.weather.load().then(data => { $weather = data; })
  • for illustration purposes, we created the store object inside the component.

in applications, it's common for multiple components to share a single state.

create a file to manage the global state as follows

weather.store.js
import { writable } from 'svelte/store' import { api } from './api' const weather = writable(undefined); export default { weather, loadWeather: async () => { const data = await api.weather.load() weather.set(data) // makes view to be updated } }

the weather.store.js that manages the state looks like this

  • state management is decoupled from components.

weather.svelte#03` imports the global state as shown below.

Weather.svelte#03
import {writable} from 'svelte/store' import {weather, loadWeather} from './weather.store' let _wdata; const unsub = weather.subscribe((weatherData) => { _wdata = weatherData }) // Pull the API, weather, out of the component. loadWeather()

let's say it's used inside another component (Slide.svelte), as shown below.

SlideMenu.svelte
<menu> <Weather/> </menu>
  • The slide menu appears on the screen only when you press the button (toggling).

A <Weahter/> component is created each time the Slide menu appears on the screen.

the callback function is registered as weather.subscribe in Weather.svelte#03 is called.

and the callback function registered in the state weather is not removed when the slide menu is closed.

what does this mean?

Slide menu is expanded every time, a callback is registered.

  • if you opened the menu 4 times, you have 4 callbacks registered on the store object weather.
  • if the state changes once, 4 callbacks are called.
  • the screen update happens 4 times.

to avoid the bug, Weather.svelte#03 should be supplemented as follows.

Weather.svelte#04
import {onDestroy} from 'svelte'; let _wdata; const unsub = weather.subscribe((weatherData) => { _wdata = weatherData }) onDestory(() => { unsub(); // removes callback internally }
  • the subscribe function of the store object must return another function (unsub) as its return value.
  • this function must implement the ability to release any callbacks registered when it is called (unsub).
  • components typically call this function on onDestory.

because it is impossible to adhere to these conventions perfectly, svelte provides a store prefix syntax.

the store prefix syntax in svelte automatically makes subscription, unsubscription work.

4. Svelte store prefix '$'

Weather.svelte#05 - script
import {writable} from 'svelte/store' import {weather, loadWeather} from './weather.store' // let _wdata; // const unsub = weather.subscribe((weatherData) => { // _wdata = weatherData // }) loadWeather()
  • remove the code to subscribe from the <script>.
  • remove the temporary variable as well.

in the HTML area, access the store object by prefixing it with store prefix.

Weather.svelte#05 - html
<div> <h5>weather</h5 <h5>weather</h5> {#if $weather} <span>{$weather.temperature}</span> <span>{$weather.humidity}</span> {:else} <span>...loading</span> {/if} </div>

if we look at weather.store.js again, we have code that updates the store object.

weather.store.js
loadWeather: async () => { const data = await api.weather.load(); weather.set(data); // makes view to be updated }
  • the store prefix cannot be used because weather.store.js is a regular js file, not a svelte file.

changing its state with store.set(...) will update the component Weather.svelte.

Weather.svelte
{#if $weather} ... {/if}

Conclusion: Whenever possible, use the store prefix when using store objects in components.

1.4. Using STORE objects in plain JS(TS) files

as already mentioned above, the STORE PREIFX syntax is only recognized within SVELTE files. in JS(TS) files, it is treated as a normal variable.

as your application grows, you may find that your STORE objects depend on each other, in which case you'll want to manage subscriptions manually.