Svelte.js 3 Stores & State Management Tutorial

In this Svelte tutorial we learn about larger scale state management and Svelte stores.

We also cover writable, read-only and custom stores, as well as how to subscribe to or unsubscribe from any of them.

Finally, we cover how to set and update values in a writable store.

Lesson Project

If you want to follow along with the examples in this lesson, you’ll need a Svelte app that was cloned with degit or scaffolded with Vite

If you already have an app from previous lessons, you can use that instead.

What is State Management?

An application’s state is how it synchronizes data among its components.

There are two main types of state.

  • Local state is the data we manage inside a component. It can only affect that component.

    An example would be a menu that’s activated with a button.

  • Global state is data that affects more than one component, or the entire application.

    An example would be a user’s authentication status.

As an application or its team grows, managing state with the Context API can become more challenging.

The Svelte store is a state management solution that helps developers overcome these challenges.

  1. Components can become bloated with data that’s necessary for other components, but not for itself.
  2. It can become confusing as to where a component’s state is changed. This can lead to unpredictable behavior, especially with a larger developer teams.
  3. Unpredictable behavior is error-prone because a developer can accidentally update the wrong state, or miss an important update to a state.

This is where the Svelte Store helps us manage our state.

  1. It detaches the state from a component and manages it in a different place, so components cannot become bloated with unnecessary data.
  2. It’s more predictable because there are clear rules to how state should be managed, updated and synced.
  3. Because we have a clearly defined flow of data through the application, it’s less error-prone.

How to create a Svelte Store

A store is a Javascript file in our project that we can import into any component.

There are multiple types of stores.

  • A readable store means that the values are read-only.
  • A writable store means that the values can be modified.
  • A custom store means that the values can be derived from other store values.

The first two types of stores have a corresponding method that we use to create the store values.

Syntax: create store
import { storeFunction } from 'svelte/store'

// create a store with a default value
const dataStore = storeFunction(defaultValue)

// make the store available
// outside of this file
export default dataStore

tip We can use any type of data for our defaultValue like an object, array, string etc.

As an example, let’s create a new store in our project.

  • /src/store/cart.js

The project should look similar to this.

Example: project
project_folder/
├── src/
|   ├── store/
|   |   └── cart.js
|   └── App.svelte

Our store will be writable so we’ll import the writable function. Then, we’ll add an array of objects with items that the user can add to their cart.

Example: src/store/cart.js
import { writable } from 'svelte/store'

const cart = writable([
  {
    id: 1,
    name: 'Item 1',
    price: 1.99
  },
  {
    id: 2,
    name: 'Item 2',
    price: 2.99
  }
])

export default cart

That’s all we need to do to create a store.

How to read data from (subscribe to) a store in Svelte

Once we have a store, we can subscribe to it in any component where we want to use its data.

Subscribing creates an ongoing subscription to the store that will read its data and inform us of any changes to that data.

To subscribe to a store, we use the subscribe method on an imported store name. The method takes a callback function as its argument where we process the data.

Syntax: subscribe
<script>
  import store from '/path/to/store.js'

  // subscribe to the store
  store.subscribe(data => {
    // process data
  })
</script>

As an example, let’s subscribe to our cart store in the root App component. In the callback, we’ll assign the data to an array and output the items to the page inside a loop.

Example: src/App.svelte
<script>
  import cart from './store/cart.js'

  let items

  cart.subscribe(cartItems => {
    items = cartItems
  })
</script>

{#each items as item (item.id)}
  <p>{item.name} - <span>${item.price}</span></p>
{/each}

If we run the example in the browser, we’ll see the two cart items that we defined in our store.

How to unsubscribe from a store in Svelte

If a component is subscribed to a store, we’ll need to ensure that it always unsubscribes as well.

That’s because every time the component is mounted, it subscribes to the store. If the component is instantiated and destroyed multiple times, it could cause a memory leak.

One way we could unsibscribe from a store is to use the onDestroy lifecycle hook to explicitly terminate the subscription.

To do that, we need to assign the subscription to a variable, then pass that variable to the onDestroy function as an argument.

Syntax: onDestroy unsubscribe
<script>
  import store from '/path/to/store.js'
  import { onDestroy } from 'svelte'

  // subscribe to the store
  const unsub = store.subscribe((data) => {
    // process data
  })

  // destroy the subscription
  onDestroy(unsub)
</script>

This is a perfectly valid approach and fine to use in your project. The problem is though that we’ll need to do this every component that subscribes to a store.

Luckily, we can let Svelte manage the subscribing and unsubscribing for us.

To do that, we simply prefix the store import name with a $ (dollar) symbol wherever we want to use it.

Syntax: managed subscription
<script>
  import store from '/path/to/store.js'

  // use $store where you want
  // to access the data

  // in the script
  $: console.log($store)
</script>

<!-- or the markup -->
<p>{$store}</p>

As an example, let’s change our root App component to use a managed subscription instead of a manual one.

Example: src/App.svelte
<script>
  import cart from './store/cart.js'

  // let items

  // cart.subscribe(cartItems => {
  //   items = cartItems
  // })
</script>

<!-- change "items" to $cart -->
{#each $cart as item (item.id)}
  <p>{item.name} - <span>${item.price}</span></p>
{/each}

If we run the example in the browser, everything still works as expected. But now our code is much shorter and simpler.

How to change a writable store's data in Svelte

Svelte gives us two methods to change a store’s data.

  • The set method updates the data with a new value.
  • The update method gives us the current store data to manipulate (add, edit or delete).

For our example, let’s create a new component that will be responsible for changing the store data.

  • /src/components/ManageStore.svelte

The project should look similar to this.

Example: project
project_folder/
├── src/
|   ├── store/
|   |   └── cart.js
|   ├── components/
|   |   └── ManageStore.svelte
|   └── App.svelte

This component will import the store and have a button that calls a function to modify the store data. We’ll implement the functionality in a bit.

Example: src/components/ManageStore.svelte
<script>
  import cart from '../store/cart.js'

  function changeData() {
    // TODO
  }
</script>

<button on:click={changeData}>Change data</button>

The root App component will import the ManageStore component and create an instance of it in the markup above the loop.

Example: src/App.svelte
<script>
  import cart from './store/cart.js'
  import ManageStore from './components/ManageStore.svelte'
</script>

<ManageStore />

{#each $cart as item (item.id)}
  <p>{item.name} - <span>${item.price}</span></p>
{/each}

If we run the example in the browser, we should see the button above the items.

How to set new data values in a writable store in Svelte

The set method will override the data from the store with whatever we specify. This includes a different type of value.

To use it, we call the method on the import name and specify the new data as an argument.

Syntax: override with set
data.set(newData)

As an example, let’s use the set method in our ManageStore component to an empty array.

Example: src/components/ManageStore.svelte
<script>
  import cart from '../store/cart.js'

  function changeData() {
    cart.set([])
  }
</script>

<button on:click={changeData}>Change data</button>

If we run the example in the browser and click on the button, the cart items disappear.

The previous array was overridden with a new empty array, and because there are no items to loop over, it won’t display anything.

How to update data in a writable store in Svelte

The update method takes a callback function as an argument, which gives us the current store data to manipulate.

The callback needs to return the new data we want to store so that will allow us to add, edit or delete items from the current data.

Syntax: update
data.update(currentData => {
  // manipulate data
  return // data
})

To demonstrate, let’s remove the set method from our example and add another item with the update method.

To add another item, we’ll spread the previous items into a new array and then add another object with the item details. If we don’t add the previous items back into the new array, they will be lost.

Example: src/components/ManageStore.svelte
<script>
  import cart from '../store/cart.js'

  function changeData() {
    cart.update(items => {
      return [
        ...items,
        {
          id: 3,
          name: 'Item 3',
          price: 3.99
        }
      ]
    })
  }
</script>

<button on:click={changeData}>Change data</button>

If we run the example in the browser and click on the button, it will update the store with the new item and show it on the page below the others.

How to create and use a custom store in Svelte

Svelte allows us to use its writable and readable stores to create our own custom stores with custom and common logic. This can make interaction with the store both simpler and faster.

To create a custom store, we need to create an object that subscribes to an existing store in the subscribe property. Following that, we can have our own custom logic.

Syntax: custom store subscribe
import { storeFunction } from 'svelte/store'

// create a regular store
const dataStore = storeFunction(defaultValue)

// create a custom store that
// subscribes to the regular one
const customStore = {
  subscribe: storeFunction.subscribe,
  // custom logic
}

// export the custom store
export default customStore

To demonstrate, let’s add a custom store to our example that subscribes to the cart store. We’ll also add the update logic into a new method.

Example: src/store/cart.js
import { writable } from 'svelte/store'

const cart = writable([
  {
    id: 1,
    name: 'Item 1',
    price: 1.99
  },
  {
    id: 2,
    name: 'Item 2',
    price: 2.99
  }
])

const customStore = {
  // subscribe to the cart store
  subscribe: cart.subscribe,
  // custom logic
  addItem(newItem) {
    cart.update(items => {
      return [...items, newItem]
    })
  }
}

// export the custom store
export default customStore

Instead of writing the update logic each time we want to update the store, we can simply call the addItem method and pass the new item to it.

In our case, that would be in the changeData function in the ManageStore component.

Example: src/components/ManageStore.svelte
<script>
  import customStore from '../store/cart.js'

  function changeData() {
    customStore.addItem({
      id: 3,
      name: 'Item 3',
      price: 3.99
    })
  }
</script>

<button on:click={changeData}>Change data</button>

If we run the example in the browser and click the button, the store updates as we expect.

This is a more elegant approach and will not only save us a lot of time, but eliminate any mistakes we might make when writing the update logic ourselves each time.

How to create and use a readable (read-only) store in Svelte

Another type of store is the readable store, which means that the any interaction from the user can’t set the value. This is typically used for autogenerated values that should be updated regularly, like a timer or geolocation.

A readable store uses the readable method which takes two arguments.

  1. The default store value.
  2. A callback function that we can use to set new values.

note We can set new values only in the callback and it cannot be from a user interaction like we did in the writable store.

Syntax: readable store
import { readable } from 'svelte/store'

// create a store with a default value
const dataStore = readable(defaultValue, (newValue) => {
  // use newValue() to update values
})

export default dataStore

To demonstrate, let’s modify our store to use readable. To keep the example simple, we’ll create a timer with the Javascript setInterval method that updates a counter variable.

Example: src/store/cart.js
import { readable } from 'svelte/store'

let counter = 0

const timer = readable(counter, (newValue) => {

  setInterval(() => {
    // set new counter value
    newValue(counter++)
  }, 800);
})

export default timer

In the root App component, we’ll import our store and output the value in the markup with autosubscription.

Example: src/App.svelte
<script>
  import timer from './store/cart.js'
</script>

<p>Counter: {$timer}</p>

If we save the files and run the example in the browser, we’ll see the counter ticking.

tip Readable stores can be used in custom stores.