As soon as you see the new $state
in Svelte 5, you might be tempted to do this:
// sharedState.svelte.js
// This won't work as export! โ
export const searchState = $state("");
<!-- App.svelte -->
<script>
import { searchState } from './sharedState.svelte.js'
function handleClick(){
// This won't work! โ
searchState = "bicycles";
}
</script
<button onclick={handleClick}>Search for bicycles</button>
The above doesn't work if you export it - and here is why:
โYou're encountering the most complicated part of Svelte 5. How reactivity works and how the compiler hides it from you.
When you export a single value like a number or string, there is no mechanism for Svelte to maintain reactivity because JavaScript doesn't offer a way to track that.โ
Mat Simon
Huge thanks to Mat Simon who explained this to me in a Bluesky thread ๐ก
Here is what I learned so far:
For exports, use $state with Objects - not Strings!
As said, we can't use a String (or a Number) directly for exporting states like we usually do in a single file component.
But, Objects in $state() get all their property values proxied automatically by Svelte v5. Wrapping your String value into an Object allows you to export that state and share it between files and components:
// sharedState.svelte.js
// Instead of this ...
// export const searchState = $state("");
// ... do this:
export const searchState = $state({ text : "" });
With that searchState
is turned into an Svelte state proxy with a getter and a setter for the .text
property.
When you import a $state Object, and then update the property via .text = "newValue"
, you're using the Svelte setter to update the state. This will then be updated in all other places where the state is used across your application:
// App.svelte
import { searchState } from './sharedState.svelte.js'
function handleClick(){
// uses the automatically created setter
searchState.text = "bicycles";
}
<button onclick={handleClick}>Search for bicycles</button>
Demo (REPL): Very basic $state example
You can choose any property name you want, as well as multiple properties per $state Object. Svelte 5 takes care of it automagically.
// number
export const countState = $state({ count : 999 });
// multiple properties (number, string)
export const anotherState = $state({ id: 123, title: "Hello World!" });
// array
export const tagsState = $state({ selectedTags: [] });
Full technical background explained by Mat Simon:
Svelte doesn't have to proxy ever method that modifies the underlying object. That would be pretty error prone. Every time JavaScript adds a new method, the Svelte team would need to adapt immediately, otherwise reactivity would break for that specific method. In reality [JavaScript proxies]((https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) are pretty cool. They offer get() and set() traps. [..] This means that the actual implementation of the array (and object) proxy is quite simple and handles all possible methods the array implements and might implement in the future. See Svelte docs for more details.
The Svelte docs state:
If $state is used with an array or a simple object, the result is a deeply reactive state proxy.
Beware: Don't re-assign Objects outside of their scope
When you use an Object they are also not states themselves! That's important to understand. If you do the following after an import, you lost reactivity:
import { searchState } from './sharedState.svelte.js';
// don't do this!
searchState = {text: "Hello world!"};
There is no way for Svelte to handle this for you. Always use the automatic Svelte getter/setter for exports/imports via
searchState.text = 'new value';
Note: In the same scope, if Objects are defined with $state(), they can be overridden. This article is only about exports.
Advanced: Use classes
There are multiple options to define your state objects, you can also use classes if you ever needed custom methods for updating values in your state: https://joyofcode.xyz/how-to-share-state-in-svelte-5#using-classes-for-reactive-state
Share state between components
So we know how to import (and update) states inside components and we know that we can use objects out of the box with $state
:
// MyComponent.svelte
import { searchState } from './sharedState.svelte.js'
function handleClick(){
searchState.text = "bicycles";
}
<button onclick={handleClick}>Search for bicycles</button>
We can even pass down the $state object as reference by a property with $props:
// App.svelte
<script>
import { searchTextState } from './data.svelte.js';
import ResultList from './ResultList.svelte';
</script>
<ResultList stateObj={searchTextState} />
<!-- ResultList.svelte -->
<script>
// reference to state object is passed down as prop of component
let {stateObj} = $props();
</script>
<p>You're searching for {stateObj.text}</p>
But how do you know that the state changed somewhere in your app when you're inside a component and want to do some processing based on that value? That's what $derived and $derived.by are for:
<!-- ResultList.svelte -->
<script>
// reference to state object is passed down as prop
let {stateObj} = $props();
// Listen for state changes
let resultString = $derived.by(() => {
console.log('state change detected', {stateObj});
// we would filter results here, do custom stuff
// for now, we just mess with the search text
let currentText = stateObj.text;
let uppercaseText = currentText.toUpperCase();
return `You are searching for ${uppercaseText}`;
});
</script>
<p>You're searching for {resultString}</p>
Simple Demo (REPL): Share $state between components (simple)
Usage with bind:value
As you might already know, there is no need to write change handler functions for text inputs. You can just use bind:value
to update the state automatically when text is entered:
<!-- App.svelte -->
<script>
import { searchTextState } from './data.svelte.js';
<script>
<SearchInput bind:stateObjPropToChange={searchTextState.text} />
<!-- SearchInput.svelte -->
<script>
let {stateObjPropToChange = $bindable() } = $props();
</script>
<label>
Search text:
<input bind:value={stateObjPropToChange} />
</label>
Usage with bind:group?
Multiple checkbox inputs can be handled with Svelte via bind-group=
.
To use this with shared state, make sure to use $bindable. And don't forget to use bind:
when passing the prop to the component:
// state.svelte.js
export const selectedColorsState = $state({selectedValues: []});
<!-- App.svelte -->
<FilterCheckboxes
title="Colors"
availableOptions={availableColorOptions}
bind:statePropToBind={selectedColorsState.selectedValues}
/>
<!-- FilterCheckboxes.svelte -->
<script>
let {
title,
availableOptions,
// important: you need to receive a bindable here,
// to update states back to parent components
statePropToBind = $bindable()
} = $props();
</script>
- Simple Demo (REPL): Search and filter with checkbox group components and v5 $state & $derived
- Full SvelteKit example (WIP): https://github.com/mandrasch/austrian-web-dev-companies
Summary
There is a big difference between using $state()
inside one file (one scope) - or using $state()
as export / import.
Happy to get your (critical) feedback for this article! ๐
More resources
- Bluesky discussion: #Svelte v5 - is this the easiest way to use $state (and $derived) with multiple checkboxes to filter data dynamically? ๐ค
- Different Ways To Share State In Svelte 5 - Joy of Code
- Lets Build A Filtering System with Svelte 5 , Sveltekit 2, Tailwind, Upstash (2024) - Lawal Adebola
- Svelte 5 - Global $state (convert stores to $state runes) - Svelte Mastery
- Svelte 5 Runes Demystified (1/4) - Signal Reactivity Basics - Peter Makes Websites Ltd
Demos:
- Very basic $state example
- Share $state between components (simple)
- Search and filter with checkbox group components and v5 $state & $derived (WIP)
- Full SvelteKit example: https://github.com/mandrasch/austrian-web-dev-companies (WIP)
Advanced: Use SvelteSet, SvelteMap, SvelteDate, etc.
Okay, objects are fine and handled by Svelte automagically - we got it.
But what about Date
, URL
and more built-in objects of standard JavaScript? And if you're more experienced in JavaScript, you might know that there are some more advanced data types (standard built-in objects):
The
Set
object lets you store unique values of any type, whether primitive values or object references.The
Map
object holds key-value pairs and remembers the original insertion order of the keys.
If you want to use these with reactive $state, you need to use their corresponding Svelte wrapper from svelte/reactivity
- MediaQuery
- SvelteDate
- SvelteMap
- SvelteSet
- SvelteURL
- SvelteURLSearchParams
The reason there is a separate SvelteSet
and SvelteMap
class (instead of just rewriting it automatically like they do with objects and arrays) is because they wanted to draw a line somewhere since they can't proxy every conceivable object. See Reactive Map, Set, Date and URL #10263 for technical details.
How can you use it? Simple as that:
// sharedState.svelte.js
import { SvelteSet } from 'svelte/reactivity'
export const selectedColors = new SvelteSet(['red'])
But beware: If you want to output a SvelteSet
, make sure to use this (or use the new $inspect:
{JSON.stringify({selectedColors: [... selectedColors]})}
Acknowledgements
Thanks very much to Mat Simon and hfcRed (Svelte Discord)!
Top comments (0)