DEV Community

Matthias Andrasch
Matthias Andrasch

Posted on • Edited on

Svelte 5: Share state between components (for Dummies)

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("");
Enter fullscreen mode Exit fullscreen mode
<!-- 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>
Enter fullscreen mode Exit fullscreen mode

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 : "" });
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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: [] });
Enter fullscreen mode Exit fullscreen mode

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!"}; 
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode
<!-- 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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} />

Enter fullscreen mode Exit fullscreen mode
<!-- SearchInput.svelte -->

<script>
    let {stateObjPropToChange = $bindable() } = $props();
</script>

<label>
   Search text:
   <input bind:value={stateObjPropToChange} />
</label>
Enter fullscreen mode Exit fullscreen mode

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: []});
Enter fullscreen mode Exit fullscreen mode
<!-- App.svelte -->
<FilterCheckboxes 
    title="Colors" 
    availableOptions={availableColorOptions} 
    bind:statePropToBind={selectedColorsState.selectedValues}
/>
Enter fullscreen mode Exit fullscreen mode
<!-- 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>
Enter fullscreen mode Exit fullscreen mode

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

Demos:

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'])
Enter fullscreen mode Exit fullscreen mode

But beware: If you want to output a SvelteSet, make sure to use this (or use the new $inspect:

{JSON.stringify({selectedColors: [... selectedColors]})}
Enter fullscreen mode Exit fullscreen mode

Acknowledgements

Thanks very much to Mat Simon and hfcRed (Svelte Discord)!

Top comments (0)