Imagine you are building a medium-sized web application. You start with a few components: a header, a profile sidebar, and a main content area. At first, passing data through “props” seems easy. But as your application grows, you find yourself passing a user’s theme preference or authentication status through ten layers of components that don’t even use the data—they just pass it along to a grandchild. This architectural nightmare is known as prop drilling.
In many frameworks, solving this requires heavy-handed libraries like Redux or Vuex, which come with a mountain of boilerplate code. Svelte takes a different approach. It provides a built-in, elegant solution called Stores. Svelte stores allow you to manage global state with minimal syntax, maximum reactivity, and zero external dependencies.
In this comprehensive guide, we will dive deep into the world of Svelte stores. Whether you are a beginner looking to understand the basics or an intermediate developer wanting to build complex custom store logic, this tutorial covers everything you need to know to write clean, maintainable, and high-performance Svelte applications.
Understanding the Store Contract
Before we write any code, it is vital to understand what a “store” actually is in Svelte. Unlike other state management systems that rely on complex dispatchers, a Svelte store is simply an object with a subscribe method.
This is known as the Store Contract. Any object that implements a subscribe method correctly is a store. This simplicity is Svelte’s superpower; it means you aren’t locked into a specific implementation. You can turn almost anything—an API response, a WebSocket connection, or even a browser resize event—into a reactive store.
The subscribe method must take a callback function as an argument. Whenever the value of the store changes, that callback is executed. Furthermore, subscribe must return an unsubscribe function to stop the component from listening once it is destroyed, preventing memory leaks.
Writable Stores: The Basics of Reactivity
The most common type of store is the writable store. As the name suggests, these stores allow both reading and writing (updating) the data from any part of your application.
Step 1: Creating a Writable Store
To create a writable store, you import the function from svelte/store and initialize it with a starting value.
// stores.js
import { writable } from 'svelte/store';
// We initialize a store named 'count' with a value of 0
export const count = writable(0);
Step 2: Updating the Store
Writable stores come with two primary methods for changing their value: set() and update().
- set(newValue): Completely replaces the current value with a new one.
- update(callback): Takes a function that receives the current value as an argument and returns the new value. This is ideal when the new state depends on the old state.
// Inside a Svelte component
import { count } from './stores.js';
// Reset the count to zero
const reset = () => count.set(0);
// Increment the count based on previous value
const increment = () => count.update(n => n + 1);
Real-world example: Think of a writable store as a shared clipboard. Anyone can read what’s on it, anyone can erase it and write something new, and anyone can append more text to the existing content.
Readable Stores: Handling External Data Streams
Sometimes you have data that should be reactive but shouldn’t be manually changed by your components. Examples include the user’s GPS coordinates, the current time, or a stream of data from a WebSocket.
A readable store takes two arguments: an initial value and a setup function. The setup function is called when the store gets its first subscriber and returns a cleanup function that runs when the last subscriber unsubscribes.
// timeStore.js
import { readable } from 'svelte/store';
export const time = readable(new Date(), function start(set) {
// Setup logic: Start an interval when someone subscribes
const interval = setInterval(() => {
set(new Date()); // Update the store value
}, 1000);
// Cleanup logic: Clear interval when no one is listening
return function stop() {
clearInterval(interval);
};
});
This pattern is extremely efficient because the code inside the start function only runs when it is actually needed by the UI, saving CPU cycles and battery life on mobile devices.
Derived Stores: Transforming State Automatically
What if you have a store, but you need a version of that data that is transformed? For example, if you have a store containing a list of users, you might want a secondary store that only contains “active” users. This is where derived stores shine.
A derived store depends on one or more other stores and recalculates its value whenever the dependencies change.
// derivedStore.js
import { writable, derived } from 'svelte/store';
export const count = writable(1);
// This store will always be double the value of count
export const doubled = derived(count, $count => $count * 2);
// You can also derive from multiple stores
export const quadrupled = derived(
[count, doubled],
([$count, $doubled]) => $count + $doubled + 1
);
Derived stores are fundamental for keeping your state logic out of your components. Instead of doing math or filtering in your .svelte files, keep your raw data in writable stores and your computed logic in derived stores. This makes your UI components much cleaner and easier to test.
The Power of the $ Auto-subscription Syntax
In standard JavaScript files, using a store requires calling .subscribe() and then manually calling the unsubscribe() function to prevent memory leaks. This is tedious.
// The "Hard" Way (Don't do this inside .svelte components)
import { count } from './stores.js';
import { onDestroy } from 'svelte';
let countValue;
const unsubscribe = count.subscribe(value => {
countValue = value;
});
onDestroy(unsubscribe); // Prevent memory leaks
Svelte provides a shortcut: the $ prefix. Whenever you prefix a store name with $ inside a Svelte component, Svelte automatically handles the subscription, the unsubscription, and makes the value reactive.
<!-- The "Svelte" Way (Do this!) -->
<script>
import { count } from './stores.js';
</script>
<h1>The count is {$count}</h1>
<button on:click={() => $count += 1}>Increment</button>
Note: You can only use the $ prefix inside Svelte components. In plain .js or .ts files, you must use the subscribe or get methods.
Building Custom Stores for Clean Architecture
A “Custom Store” sounds intimidating, but it is actually one of Svelte’s most useful features. A custom store is just a regular JavaScript object that includes a subscribe method but encapsulates its own update logic. This allows you to expose domain-specific methods instead of raw set and update.
Example: A Toggle Store
Instead of manually setting true/false, let’s create a store that knows how to “toggle” itself.
// toggleStore.js
import { writable } from 'svelte/store';
function createToggle(initialValue = false) {
const { subscribe, set, update } = writable(initialValue);
return {
subscribe,
toggle: () => update(v => !v),
open: () => set(true),
close: () => set(false),
reset: () => set(initialValue)
};
}
export const sideBarOpen = createToggle(false);
Using this in a component is much more expressive:
<script>
import { sideBarOpen } from './toggleStore.js';
</script>
<button on:click={sideBarOpen.toggle}>
{ $sideBarOpen ? 'Close Menu' : 'Open Menu' }
</button>
By building custom stores, you are creating a “service layer” for your application. Your components no longer care how the data changes; they just call methods like .login(), .addToCart(), or .toggle().
Persistence: Linking Stores to LocalStorage
A common requirement is to keep data alive even after the user refreshes the page. We can achieve this by creating a store that interacts with the browser’s localStorage.
Here is a reusable function to create a persistent store:
// persistentStore.js
import { writable } from 'svelte/store';
export function persistentWritable(key, defaultValue) {
// Get initial value from localStorage if it exists
const storedValue = localStorage.getItem(key);
const initial = storedValue ? JSON.parse(storedValue) : defaultValue;
const store = writable(initial);
// Subscribe to changes and update localStorage
store.subscribe(value => {
localStorage.setItem(key, JSON.stringify(value));
});
return store;
}
// Usage:
export const theme = persistentWritable('theme', 'light');
With this setup, any time you update $theme, it is automatically saved to the browser’s storage. When the user returns, the value is restored instantly.
Common Mistakes and How to Avoid Them
1. Forgetting to Unsubscribe (in JS files)
While Svelte handles unsubscription in .svelte files via the $ prefix, it does not do this in plain JavaScript files. If you call count.subscribe(...) inside a standard script, you must store the returned function and call it when that script’s logic is no longer needed. Failure to do so leads to memory leaks where the store continues to try and update variables that should have been garbage collected.
2. Overusing Stores
Beginners often put every variable into a store. Stores are meant for global state. If a piece of data is only used by one component and its immediate children, standard props or local variables are faster and more memory-efficient.
3. Mutating Complex Objects Directly
If your store contains an object or an array, you must ensure you are returning a new reference when updating. Svelte’s reactivity is triggered by assignment.
// ❌ INCORRECT: This might not trigger a UI update
userStore.update(u => {
u.name = 'John';
return u;
});
// ✅ CORRECT: Create a new object reference
userStore.update(u => {
return { ...u, name: 'John' };
});
4. Side Effects Inside Subscribers
Avoid putting heavy logic or API calls directly inside a .subscribe() callback. Subscribers should ideally be “pure” functions that simply update the UI or sync data. If you need to trigger an action when a store changes, consider using Svelte reactive statements ($:) within a component instead.
Summary and Key Takeaways
Svelte stores offer a refreshing take on state management by prioritizing simplicity and the “Store Contract.” Here is what we have learned:
- Writable Stores: Use these for data that needs to be read and changed globally (e.g., User authentication).
- Readable Stores: Use these for external data sources like timers, sensor data, or WebSockets.
- Derived Stores: Use these to calculate new data based on existing stores (e.g., filtering a list).
- The $ Prefix: Always use this in
.sveltefiles to automatically manage subscriptions and unsubscriptions. - Custom Stores: Encapsulate logic by returning an object that includes the
subscribemethod but restricts how the data is modified. - Immutability: Always return new object or array references when updating stores containing complex data types.
Frequently Asked Questions
Can I use Svelte stores with other frameworks?
Yes! Because Svelte stores are based on a simple JavaScript contract (the subscribe method), they can be used in React, Vue, or even vanilla JS projects. You just won’t have the $ auto-subscription syntax available.
What is the difference between update() and set()?
set() takes a direct value and replaces the store content. update() takes a function that allows you to calculate the new value based on the current value. Use update() for counters or appending items to arrays.
Should I use stores or context?
Svelte’s getContext and setContext are not inherently reactive. They are used to pass data down a component tree without prop drilling. Stores, on the other hand, are reactive. Frequently, developers put a store inside a context to get the best of both worlds: reactive data that is only available to a specific sub-tree of components.
Are Svelte stores fast enough for high-frequency data?
Absolutely. Svelte stores have very little overhead. They are essentially a list of callback functions. For extremely high-frequency data (like 60fps animation state), stores are significantly more efficient than Redux or React State because they don’t trigger a full component tree “re-render” unless the specific piece of data changes.
