State Management with Svelte - Stores (Part 3)

Disclaimer - If you are unfamiliar with the Context API in Svelte applications, then please read this blog post before proceeding on. You must understand the limitations of the Context API to better understand stores and how they address those limitations.

For components in a Svelte application to share data irregardless of the subtree they belong to in the component hierarchy, Svelte provides stores for handling global state via the svelte/store module. Unlike the Context API, which involved setting contexts directly in the <script /> block of a component via setContext, stores can be created outside of a component in their own dedicated modules. Yet, like the Context API, a store can only contain a single value. This value can be a primitive, an object, an array, etc.

When a component subscribes to a store, the component can receive the updated value from the store and re-render accordingly. Like props, stores are reactive. Stores are designed to cover practical use cases such as theming, accommodating internalization/localization (i18n), persisting a logged-in user's information, etc.

Svelte supports four types of stores:

  • Readable Stores (readable) - Components accessing readable stores have permission to only read data from them.

  • Writable Stores (writable) - Components accessing writable stores have permission to read and modify data via only set and update methods.

  • Derived Stores (derive) - Derive value from one or more stores.

  • Custom Stores - An object that implements the store contract and does not need to rely on any of Svelte's built-in stores. It must contain, at the very least, a properly implemented subscribe method and unsubscribe method will be recognized and treated as a valid store. Additional custom methods, such as a stricter setter method with extra validation checks, can be attached to this object. Like the natively available stores, a component can auto-subscribe to this custom store and access its value via the $ prefix syntax.

Readable Stores#

A readable store is a store whose value cannot be set within a component subscribed to it. Components can only read from it and cannot write to it. To create a readable store, first import the readable function from the svelte/store module:

Then, call the readable function. It accepts two arguments:

  • The initial value of the store.

  • A function that provides a set function, which replaces the initial value of the store, and returns a stop function, which is executed when the subscriber count decreases from one to zero. This set function is primarily used for setting the store value to data fetched from an API.

    const store = readable([], async (set) => {

    const response = await fetch(API_ENDPOINT');

    const data = response.json();

    set(data);

    return () => {};

    });

    All changes to the store's value must be done in this function. In fact, using the setInterval function can automatically change the store's value every x milliseconds. let value = 0;

    const store = readable(0, async (set) => {

    const id = setInterval(() => {

    set(++value);

    }, 1000);

    return () => {

    clearInterval(id);

    };

    });
    This function will be executed when a component subscribes to the store as its first subscriber. This function will not be called when additional components subscribe to the store. However, if all of the components subscribed to the store unsubscribe from it (subscriber count reaches zero), then the first component to subscribe to the store after will cause this function to be called again.

The readable function returns an object containing a subscribe method, which allows a component to subscribe to the store and listen for changes to its value.

The subscribe method accepts a single argument, a function that provides the store's value. Commonly, a component variable is set to this value so that it can be displayed or used for the component's internal state. This method returns an unsubscribe method, which allows the component to unsubscribe from the store. The unsubscribe method is called within the onDestroy lifecycle method to avoid memory leaks, which can result from the component being instantiated and being destroyed many times, leaving subscriptions of previous component instances still in memory.

Example #1:

A component subscribes to a readable store with the value "Hello World!" It sets the message variable to this value, and this message is rendered within the <h1 /> element. The UI will display "Hello World!" in big, bold text.

(Component.svelte)

Example #2:

<ComponentA /> and <ComponentB /> both subscribe to a readable store imported from an external module, stores.js. Whichever component first subscribes to the store will trigger the function passed as a second argument to the readable method, which will print "Only called once!" to the developer tools console and replace the default value of "Hello World!" with "Lorem Ipsum." When the other component subscribes to the store, the function passed as a second argument to the readable method will not be triggered.

When these components subscribe to the store, their message variable will be set to the store's value, now "Lorem Ipsum," and this message is rendered within the <h1 /> element. The UI will display two "Lorem Ipsum" in big, bold text.

(stores.js)

(ComponentA.svelte/ComponentB.svelte)

(Parent.svelte)

Writable Stores#

A writable store is a store whose value can be set within a component subscribed to it via pre-defined methods. Components can read from and write to it. To create a writable store, first import the writable function from the svelte/store module:

Then, call the writable function. It accepts the same two arguments as the readable function mentioned above: an initial value of the store and a function that provides a set function for setting the value of the store.

The writable function returns an object containing three methods:

  • subscribe - Allows a component to subscribe to the store and listen for changes to its value.

  • set - Allows a component to completely override the value currently set in the store. It accepts a single argument, which is the value to be set. If the value of the store is already set to this value (equal), then the store's value is not updated.

  • update - Allows a component to update the store's value based on the current value in the store (not mandatory). It accepts a single argument, which is a function that provides the current value in the store as an argument and returns the new value to be set to the store.

Example #1:

A component subscribes to a writable store with the value zero. It sets the count variable to this value, and this count is rendered within the <h1 /> element. Initially, the UI will display 0 in big, bold text. Along with this text, the UI will present two buttons, one for incrementing the count by one each time it is clicked and one for resetting the count back to zero.

To increment the count by one, use the store's update method, which accepts a function that provides the current value of the store as an argument and returns the new value to be set to the store.

When the "Increment" button is clicked, the count rendered within the <h1 /> element will be updated to the newly incremented value.

To reset the count to zero, use the store's set method, which accepts the value to be set to the store. Pass it the value zero.

When the "Reset" button is clicked, the count rendered within the <h1 /> element will be updated to zero.

(Component.svelte)

Example #2:

<ComponentA /> and <ComponentB /> both subscribe to a writable store imported from an external module, stores.js. Whichever component first subscribes to the store will trigger the function passed as a second argument to the writable method, which will print "Only called once!" to the developer tools console and replace the default value of zero with one-hundred. When the other component subscribes to the store, the function passed as a second argument to the writable method will not be triggered.

When these components subscribe to the store, their count variable will be set to the store's value, now one-hundred, and this count is rendered within the <h1 /> element. The UI will display two "100" in big, bold text along with two sets of "Increment" and "Reset" buttons. When any of the "Increment" buttons is clicked, the two count values rendered will be incremented by one (store values are reactive, and both components are subscribed to the same store). When any of the "Reset" buttons is clicked, the two count values rendered will be changed to "0."

(stores.js)

(ComponentA.svelte/ComponentB.svelte)

(Parent.svelte)

Derived Stores#

A derived store is a store whose value is derived from values from at least one store. Because the derived value depends on other stores, its value cannot be set by a component. Components can only read from it and cannot write to it. To create a derived store, first import the derived function from the svelte/store module:

Then, call the derived function. It accepts three arguments:

  • The stores the derived store depend on. If the derived store depends on a single store, then that store's instance can be passed directly. If the derived store depends on more than one store, then those store instances must be passed within an array.

  • A function that provides the values of the dependency stores and a set function, which sets the value of the derived store. This function is executed whenever a value in any of these dependency stores changes, and it returns a derived value. The values of these dependency stores are provided as either a single value or an array of values if only a single store is passed or multiple stores are passed (as an array) respectively.

    Example #1 (Single Store):

    const strsStore = writable([]);

    // This store contains the total number of characters in all of the strings. const totalCharsStore = derived(strsStore, $strs => $strs.reduce((total, str) => total += str.length, 0));

    Example #2 (Single Store):

    const rgbStore = writable();

    // This store contains alternative representations of the RGB color.

    const conversionStore = derived(rgbStore, $rgb => ({

    hex: rgbToHex($rgb),

    hsl: rgbToHsl($rgb),

    hwb: rgbToHwb($rgb),

    cmyk: rgbToCmyk($rgb),

    ncol: rgbToNcol($rgb)

    }));

    Example #3 (Multiple Stores):

    const salesTaxStore = writable(0.0875);

    const tipStore = writable(0.15);

    const subTotalStore = writable(0);

    // This store calculates the restaurant bill. const totalStore = derived([ salesTaxStore, tipStore, subTotalStore ], ([ $salesTax, $tip, $subTotal ]) => subTotal + salesTax subTotal + tip subTotal);

    This function can set a derived value asynchronously via the set function. Example #4 (Asynchronous):

    const recentSongsStore = writable([]);

    const recommendedSongsStore = derived(recentSongsStore, $recentSongs => { fetch(GET_RECOMMENDED_SONGS_API_ENDPOINT, { method: 'post', body: JSON.stringify({ recentSongs: $recentSongs }) }) .then(response => response.json()) .then(recommendedSongs => { set(recommendedSongs) }); });

    Or...

    const recentSongsStore = writable([]);

    const recommendedSongsStore = derived(recentSongsStore, async ($recentSongs) => {

    const response = await fetch(GET_RECOMMENDED_SONGS_API_ENDPOINT, {

    method: 'post',

    body: JSON.stringify({

    recentSongs: $recentSongs

    })

    });

    const recommendedSongs = await response.json();

    return recommendedSongs;

    });

    If this function (the "callback") returns a function, then this function will be called when...

    • A value of one of the dependency stores changes and causes the "callback" to re-run.

    • The derived store's subscriber count reaches zero.

    Example #5:

    const currentExamStore = writable(EXAM_A);

    // If the user enters an online examination portal, and they are required to complete exams in a fixed amount of time, then the UI might update the status of the exam from 'IN_PROGRESS' to 'FINISHED' after this amount of time has elapsed and prevent the user from performing additional actions as the exam is being graded. If the user exits the exam early, then the timer needs to be cleared before they select a different exam to complete.

    const examStatusStore = derived(currentExamStore, $currentExam => {

    const id = setTimeout(() => {

    set('FINISHED');

    }, $currentExam.timeAllowed);

    return () => {

    clearTimeout(id);

    };

    });

  • (Optional) The initial value of the derived store. This value is set before the set function is called by the second argument function.

    const storeA = readable(100);

    const storeB = writable(200);

    const store = derived([ storeA, storeB ], ([ $a, $b ], _set) => $a + $b, 0); // Here, the initial value of store is zero.

Like the readable function, the derived function returns an object containing a subscribe method, which allows a component to subscribe to the store and listen for changes to its value.

Example:

A component subscribes to both a writable store, which contains an RGB value (an array of three numbers, each representing an individual channel of an RGB value), and a derived store, which contains an HSL value derived from this writable store's RGB value. Both are set with default values representing the color white in their respective formats: [255, 255, 255] and [0, 0, 1].

The inputs under the "RGB" section of the UI are each bound to a value in the rgb array, which is set to the value of the RGB store (rgbStore). The inputs under the "HSL" section of the UI are each bound to a value in the hsl array, which is set to the value of the HSL store (hslStore).

When the user changes the value of any one of the three inputs (restricted to numbers within the range 0 to 255) under the "RGB" section of the UI, the values of the three inputs under the "HSL" section of the UI will be changed to the HSL value equivalent to this RGB value.

Svelte Derived Store Demo - RGB-HSL Conversion

(utils.js)

(stores.js)

(Component.svelte)

Auto-Subscription ($ Prefix)#

Thus far, subscribing to a store in a component involves a lot of boilerplate code:

  1. Subscribe to each store by calling its subscribe method.

  2. Assign each store's value to a variable in the component in the subscribe method's function argument.

  3. Destructure out the store's unsubscribe method from the object returned by the subscribe method.

  4. Import the onDestroy lifecycle method.

  5. Schedule the component's unsubscription from each store within the onDestroy lifecycle method's callback.

This boilerplate code grows proportionally to the number of stores the component subscribes to.

For a component that is instantiated and destroyed many times, it is important to unsubscribe the component from the stores it subscribes to. Otherwise, the component's subscriptions remain in memory long after it's been destroyed, which will cause a memory leak.

Fortunately, Svelte provides a convenient shortcut for components to auto-subscribe to stores. By prefixing the imported store's name with $, the component automatically subscribes to the store and unsubscribes from the store when it is destroyed. Plus, the component can reference the store's value in its template without having to assign it to an extra variable and receive updates to this value.

Let's rewrite the component in the derived store example using auto-subscriptions:

To try it out, visit the demo here.

(stores.js)

(Component.svelte)

Wow! Talk about a much cleaner approach! No longer will you need to manually subscribe to each store and write all of the unsubscription-related code involving the onDestroy lifecycle method. Under-the-hood, auto-subscriptions automatically handles all of this logic. With auto-subscriptions, you can name the store after the value it contains rather than just naming it as a store. In the above example, notice how the previously named rgbStore is now named rgb. This way, when its value is referenced in a component's template via the $ prefix, the name will reflect the value itself ($rgb). Also, an input can bind directly to the writable store's value and update it whenever the user changes the input's value.

For writable stores, assignments done directly to these $-prefixed variables will call the store's set method using the value to be assigned.

This will set the rgb store's value to [0, 0, 0] upon the component's instantiation. Changing the rgb store's value will cause the derived value in the hsl store to be set to [0, 0, 0].

get#

To retrieve the value of a store once (non-reactive), use the svelte/store module's get function. This is useful for allowing components to access values from stores they are not subscribed to, but yet, may need to access once in a while.

Under-the-hood, get subscribes the component to the store, reads its value, and then, unsubscribes the component from the store all in one method.

Let's take the refactored derived store example that uses auto-subscriptions, and rewrite the component to only convert the RGB value entered into the inputs to its HSL equivalent when the user clicks on a "Convert" button:

To try it out, visit the demo here.

(Component.svelte)

Here, changes to the RGB inputs no longer automatically update the HSL inputs. When the user changes the value of any of the inputs in the RGB section, they must now click the "Convert" button, which retrieves the HSL value from the hsl store. The get method allows components to access such store values only when necessary.

Custom Stores (Store Contract)#

A custom store is an object that provides, at the minimum, the same functionality as any one of the native stores (readable or writable), and that can also provide additional functionality geared more towards domain-specific logic. When this object contains a "properly implemented" subscribe method, the object fulfills the store contract. This allows the object to be treated as a store. Its value becomes reactive, and the store can be referenced with the $ auto-subscription prefix.

A "properly implemented" subscribe method must accept a function as an argument. This function must provide the store's value as an argument. Upon subscribe being called or the store's value being updated, this function will be called immediately (and synchronously), providing the store's current value as its argument. Additionally, this function must either...

  • Return a function that unsubscribes a listening component from the store.

  • Return an object containing a function that unsubscribes a listening component from the store.

Referring back to the the svelte/store module's readable and writable methods, these methods provide minimal store implementations that fulfill the store contract:

When a custom store optionally provides a set method, which sets the store's value and calls all of the store's active subscription functions, the store becomes a writable store.

Let's take the refactored derived store example that uses auto-subscriptions, and modify the rgb store to provide additional methods for resetting the store's value to something pre-defined. In this case, setting the store's RGB value to red's, blue's, green's, black's or white's RGB value.

To try it out, visit the demo here.

(stores.js)

If the custom store relies on readable or writable, then destructure out the methods they return (subscribe, and for writable specifically, set and update) and return them in the object representing the custom store. Group all of this code within an IIFE (Immediately Invoked Function Expression), which will contain all of the custom store's implementation details while not interfering with other custom stores that rely on readable or writable.

The rgb custom store, previously a pure writable store, now provides five additional methods (resetToBlack, resetToWhite, resetToRed, resetToGreen and resetToBlue), each one setting this store's value to a pre-defined RGB value.

(Component.svelte)

Components can auto-subscribe to custom stores in the same way they can to pure readable, writable and derived stores. To access any of the custom store's methods, simply reference it via its corresponding property in its object representation. For example, rgb.resetToBlack references the resetToBlack method of the rgb store. If the user clicks on any one of the five buttons, then the value of the inputs under the RGB section will be changed to a pre-defined RGB value, and the inputs under the HSL section will also be changed due to them binding to a value of a derived store (hsl) that depends on the rgb custom store.

Often, custom stores are useful for augmenting the capabilities of writable stores, such as validating a value prior to setting it as the store's value.

Use Case - E-Commerce Shopping Cart#

Major e-commerce websites, such as Amazon, feature shopping cart buttons in their navigation bars. For a user to add a product to their shopping cart, they must first visit the product's detail page and press a button to add this product to their shopping cart. The shopping cart will be updated with the product. At any time, the user can remove the product from their shopping cart. Because components representing a product page and a navigation bar are likely to not have an ancestor-descendant relationship in a component hierarchy, to communicate with each other, they will both need to connect to a single store containing cart-related data.

Svelte Store Demo - E-Commerce Shopping Cart

Svelte Store Demo - E-Commerce Shopping Cart

(App.svelte)

The demo displays a hypothetical e-commerce webpage. The <Header /> component serves as the navigation bar for this webpage, and it contains the shopping cart button. The <Products /> component shows all of the available products for the user to add to their cart and purchase later.

(stores.js)

This demo application involves four stores:

  • productStore - A readable store that contains the product listing.

  • validCouponStore - A readable store that contains a list of the valid coupons. In a production environment, you should not fetch a list of valid coupons. Check for a valid coupon via an API endpoint with throttling enabled.

  • cartStore - A custom writable store that contains the products added to the cart (items), the valid coupons applied by the user (coupons), the number of each product the user has in the cart (quantities) and the total products in the cart (totalItems). This store provides four custom methods: addItem (add a product to the cart), removeItem (remove a product from the cart), addCoupon (apply a valid coupon) and removeCoupon (unapply a valid coupon).

  • costStore - A derived store that contains the cart's sub-total (the total cost of all the products in the cart excluding discounts, subTotal), the amount discounted from the sub-total due to coupons (discountAmount) and the final total cost (total). This store depends on the cartStore, so when a new product is added or removed from the cartStore, the cart's sub-total, discount amount and total will automatically be updated to reflect that change.

(Header.svelte)

The <Header /> component serves as a navigation bar. Here, it has a "Log In" link and a shopping cart button.

(Cart.svelte)

When the shopping cart button is clicked, a dropdown will appear. When the user adds a product to their cart, this dropdown will display this product, the sub-total of the entire cart, the amount to discount from the sub-total, an input for applying a coupon and a total (calculated as the difference of the sub-total and the discount).

Shopping Cart

In this component, the auto-subscription $ prefix is used to directly access the cartStore's and costStore's values in the template code for rendering the cart's products, discounts and costs. Additionally, auto-subscriptions automatically handle store subscriptions and unsubscriptions.

When a coupon is applied, it is checked against the list of valid coupons in $validCouponStore. If the coupon is found and recognized as a valid coupon, then it is added to the cartStore, which will then cause costStore to update the discount amount and total. A valid coupon can only be applied once.

(Products.svelte)