zustand is an excellent library primarily used in the React community. It’s my go-to choice for any shared state management in React due to its ease of use, and for being a solution to most shared state issues like zombie childs, react concurrency, and context loss between mixed renderers.
For these same reasons, I also started using it in some Svelte projects. I like how short and simple most zustand
stores look. Even though Svelte built-in store mechanisms are excellent, we can go up a level by combining the vanilla solution and zustand
. We can have the best of both worlds.
What’s a Svelte store?
According to Svelte’s docs, which I recommend reading, a Svelte store can be any object with a subscribe method. However, it’s recommended that you use Svelte’s tools to create writable
, readable
, derived
, or custom stores. Personally, I most often default to custom stores.
An example of one such store:
// src/lib/counter.store.ts
import { writable } from 'svelte/store';
function createCount() {
const { subscribe, set, update } = writable<number>(0);
return {
subscribe,
increment: () => update((n) => n + 1),
reset: () => set(0)
};
}
const countStore = createCount();
export default countStore;
To use the store in Svelte, you can do this:
// src/routes/+page.svelte
<script>
import counterStore from '../lib/counter.store';
</script>
<h1>Welcome!</h1>
<p>{$counterStore}</p>
<button on:click={() => counterStore.increment()}>Click</button>
The good thing about native Svelte stores is the auto-subscription
feature. Within a Svelte component, you can automatically use the dollar sign to subscribe to a Svelte store. In the markup we use $counterStore
. This allows us to read the store value immediately and react to its updates. That markup will always contain the latest $counterStore
value.
In this example, our value is just a number, but the store’s value can be anything, an object even!
Zustand
These Svelte stores are more than good enough! I’ve shipped production apps that used this to great effect. However, zustand
has some advantages that can be very useful! You can connect zustand
stores to Redux devtools using their devtools
middleware. There is also a middleware that you can use to persist any store with any storage provider. You can also easily craft your middleware, for example, to connect your store to the URL hash to save filters on a complex table component.
Let’s turn our previous counter
store into a zustand
store:
// src/lib/counter.store.ts
import create from 'zustand/vanilla';
import zustandToSvelte from './zustandToSvelte';
export interface CounterState {
value: number;
increment: () => void;
}
const counterStore = create<CounterState>((set) => ({
value: 0,
increment: () => set((state) => ({ value: state.value + 1 }))
}));
export default counterStore;
zustand
can infer types from your store (if you use their combine
utility), but it’s recommended to write them yourself. You must also type the actions themselves, as values and actions are all part of the store.
Still, this is not ready to be used by Svelte. We need to turn this into a native store first! zustand
stores have a subscribe
method, but it’s not natively compatible with Svelte. Let’s create a wrapper that turns a zustand
store into a compatible Svelte store.
// src/lib/zustandToSvelte.ts
import { readable } from 'svelte/store';
import type { StoreApi } from 'zustand';
export default function zustandToSvelte<StateType>(zustandStore: StoreApi<StateType>) {
const svelteStore = readable(zustandStore.getState(), (set) => {
zustandStore.subscribe((value) => set(value));
});
return {
...zustandStore,
subscribe: svelteStore.subscribe
};
}
Here we create a readable Svelte store from our zustand store. readable
takes two arguments: the initial state and a function that can set
the state of the readable. In this function, we can subscribe
to our store and set
the state of the native Svelte readable store.
Then, we return the original zustandStore
with the overridden subscribe
method from our readable Svelte store. zustand
subscribe doesn’t work by default on Svelte components. That’s why we need to go through all of these steps.
We can then use our store like this:
// src/routes/+page.svelte
<script>
import counterStore from '../lib/counter.store';
</script>
<h1>Welcome!</h1>
<p>{$counterStore.value}</p>
<button on:click={() => $counterStore.increment()}>Click</button>
Unlike a native Svelte store, the whole store object must be subscribed to. In our native example, you can call counterStore.increment()
without the dollar sign. With these new zustand stores, you’ll always have to use the dollar sign: $counterStore.increment()
.
Outside of Svelte components, you can use its methods as usual:
import counterStore from '../lib/counter.store';
// You can peek into the state or even call some actions
counterStore.getState().increment()
console.log(counterStore.getState().value)
// You can set the state
counterStore.setState({ value: 0 })
// You can also subscribe to it!
counterStore.subscribe((state) => console.log(state));
Final thoughts
To be honest, Svelte
custom stores are almost as powerful as zustand
. I’m just more familiar with the latter, and their middleware tools are also a great nice to have. However, this pattern can be applied to many state management technologies. Whether you have some redux
store or xstate
machine that you want to use within Svelte components, you can reach for svelte/readable
to try and make it work!
Stay safe!
Top comments (0)