Svelte Stores

In this module, we will learn how to share state between unrelated components through the use of Svelte stores.
Table of Contents
Svelte StoresView the code for this module.
Course Version History
  • Nov. 21, 2022 - Updated to SvelteKit v1.0.0-next.549. Changed index.svelte to +page.svelte.

At this point in the course, we are familiar with how data can be passed between components, even deeply nested components with the Context API, but not all application state belongs inside the application's component hierarchy. The app is bound to have values that need to be accessed by multiple unrelated components, or a regular JavaScript module.

In this diagram of nested components, we may have some state gathered in component E that component D also needs to have access to. In this case, since the two components are not related, the only way to do this would be to pass data up and down through the tree. This method, however, will quickly become messy and hard to manage as the component tree grows. Wouldn’t it be nice to store the data in a single centralized location? Svelte comes with built in state management, so we can do this by creating a Svelte store without having to install an additional package. A store is a global object with a subscribe method that allows components to be notified whenever the store value changes. We can create a central store of data where we can register data, and then any component that needs access to this data can subscribe to this store. We can even update this data directly from the subscribed components, and any other subscribed component will automatically get the updated data. This makes it much easier to manage our data, especially as the application grows.

Currently in our project, within our layout file we are importing our shopping cart component which accepts a bound property, cartItems. Since it is bound, any time we change the quantity of an item in our cart, the parent is also updated. We can also add a new item to our cart. Clicking the 'add to cart' button in our shopping cart dispatches an event to the layout which adds the newItem to our catItems array. In this case we have a single source of truth, cartItems, which lives in our layout component.

Now, I’ve added a new button to our GridTile component, so now we can add items to our cart from this component as well. The issue here is, this component is not being imported in our layout, it being used in our root page. How can we update our cart when we click these new 'add to cart' buttons while maintaining a single source of truth? This is where stores come in handy.

In our project, let’s create a store. First, create a new file called Store.js in out src root. Notice this is a .js file, not .svelte. We are not creating a Svelte component here, we are just creating a store to store our data. Now in this file, the first thing we need to do is import writable from svelte/store like this.

Store.js
import { writable } from svelte/store

Writable means we can both write and read from this store, and is probably the most common type of store. writable has three methods, subscribe, set, and update. subscribe will subscribe a component to the store so that the component can read and write to it. set takes one argument which is the value to be set, and update takes one argument which is a callback. The callback takes the existing store value as its argument and returns the new value to be set to the store.

Now that we have writable imported, we need to actually create the store. We can do this by invoking writable like this.

Store.js
import { writable } from svelte/store
 
const cart = writable();

Now we have a writable store of data. We can pass in some initial data into this store if we’d like. Let’s go ahead and paste in our carItems array.

Store.js
import { writable } from 'svelte/store';
 
const CartItemsStore = writable([
  {
    name: 'Sticker',
    src: 'https://cdn.shopify.com/s/files/1/0434/0285/4564/products/Sticker-mock.png?v=1623256356',
    price: '$8.00',
    quantity: 1,
  },
]);
 
export default CartItemsStore;

Now this writable store is storing this array of data. Next, we need to export this cart items store. Now we can subscribe to this from any component. Let’s first move into our ShoppingCart component, and we can import this store like this.

ShoppingCart.svelte
import CartItemsStore from '../Store.js';

Now that we are importing the store, we can use the subscribe method to subscribe to the data within it. Below where we import the store we can just say CartItemsStore.subscribe() which will fire a callback function. This callback takes the data we get from the store as a parameter. This parameter, data, will be whatever data is currently in the store. We can test this out by logging it in the console for now. Now, if we check this out in the browser, we’ll see that we have successfully subscribed to this store and the data is being logged in the console. Whenever this data changes from anywhere in the app, this callback function will be re-fired, and the new updated data will be passed to it.

Now, instead of accepting cartItems as a prop, let’s instead set cartItems equal to the array returned from our store.

ShoppingCart.svelte
import CartItemsStore from '../Store.js';
 
let cartItems;
CartItemsStore.subscribe((data) => {
  cartItems = data;
});

Now, we need to update the store when the cart changes. Currently we are using the subscribe method to read from the store, but in this case we want to write to it. We can do this using the update method.

Right now when we click our ‘add to cart’ button, we are dispatching an event with our newItem data to our parent component where the newItem was then being added to the cart. Instead, let’s use the update method to update our store with the new item directly from the shopping cart component. We can say CartItemsStore.update() and this will once again fire a callback function that takes in the current data in our store as a param, and from this callback we need to return the value of the updated data. For example, if we were to return an empty array, the whole value of our Store will become an empty array, and we would see our cart is empty.

CartItemsStore.update((currentData) => {
  return [];
});

That’s not what we want to do here. Instead, we want to return an array with all the current items, as well as the new one. Let’s return an array with our new item as well as all the current items in our store like this.

<script>
  function addItemToCart() {
    CartItemsStore.update((currentData) => {
      return [newItem, ...currentData];
    });
  }
</script>

Now when we click the 'add to cart' button, our store will be updated with the new item. It’s important to remember that anytime our store is updated, the callback will be fired and this data will be updated wherever the store is being subscribed to. When we updated our store, our subscribe method that we added earlier is called again, so our cart will update automatically.

The header, however, is not updated with the new car item. This is because we are not using a single source of truth right now. The header component is still getting cartItems from our layout component, which the cart is no longer bound to. Back in our layout file, let’s once again import and subscribe to our store and set cartItems to our stores data like this.

+layout.svelte
import CartItemsStore from '../Store.js';
 
let cartItems;
CartItemsStore.subscribe((data) => {
  cartItems = data;
});

Now, when we add the new item to our cart, the header will also be updated. Remember, anytime the store changes, every component that is subscribed to it will automatically be updated.

Finally, we can head into our GridTile component and update our store when we click the 'add to cart' button here as well. Now, a new item can be added to our cart from this component, and both the header and shopping cart components will be updated even though they are unrelated to our GridTile component.

There is, however, a subtle bug in our current code. The store is subscribed to, but never unsubscribed. If the component was instantiated and destroyed many times, this would result in a memory leak, so we need to unsubscribe from the store. Calling a subscribe method returns an unsubscribe function, so let’s go ahead and declare unsubscribe in our layout page, and set it to your subscribe method. Now, we can call this on through the onDestroy lifecycle hook.

+page.svelte
import CartItemsStore from '../Store.js';
 
let cartItems;
const unsubscribe = CartItemsStore.subscribe((data) => {
  cartItems = data;
});
import BlogStore from '../../Store.js';
 
onDestroy(unsubscribe);

This works, but it starts to resemble a boilerplate, especially if your component subscribes to multiple stores. Instead, Svelte has a nifty trick we can use. We can reference a store value by prefixing the store name with a dollar sign. Instead we can say:

cartItems = $CartItemsStore;

Much simpler! We call this auto-subscribing to the store, and it works because any name beginning with $ is assumed to refer to a store value. This simplifies our code a lot, and also fixes our bug! Anywhere we subscript to the store, we can update it with this shorthand!

Now that we know how to manage our applications state with stores, let’s learn how to dynamically change component templates through the use of slots in the next module.