State management is a big deal in web development. It keeps your app's data flowing smoothly and ensures everything works as expected.
In this guide, we'll break down state management in Svelte. We'll cover essential concepts like reactivity and stores and show you how to use them in your projects. By the end, you'll have a solid grasp on how to handle state in Svelte.
Let's get started!
Prerequisites
You should be familiar with the following technologies to follow along this guide:
Basic understanding of Svelte
HTML, CSS, and JavaScript knowledge
Reactivity
Reactivity is at the core of Svelte's state management. This feature is the engine that drives the synchronization between the Document Object Model (DOM) and the application's state.
Let's explore some mechanisms you can use to achieve reactivity within your Svelte application.
Variable Assignments
The simplest form of reactivity in Svelte is through the use of variables. Variables in Svelte are reactive by default, meaning any change to their value (by reassigning them) triggers an update in the corresponding DOM elements.
Let's look at an example below:
<script>
let count = 0;
let timestamps = [];
function increment() {
count += 1;
timestamps = [...timestamps, new Date(Date.now())]
}
</script>
<button on:click={increment}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<ul>
{#each timestamps as timestamp, index}
<li>Click {index + 1} was at {timestamp.toLocaleString()}</li>
{/each}
</ul>
In this example, the count
and timestamps
variables are reactive. Whenever their value changes (in this case, when the button is clicked), Svelte automatically updates the DOM to reflect the new values.
Reactive Declarations
Reactive declarations allow you to define reactive variables derived from other states. By using reactive declarations to define derived states, you can ensure that any changes to their dependencies trigger an update to their value.
From the previous example, let's say you want a text under the button that shows the value of count
multiplied by 2. You can create a doubled
variable as a reactive value:
<script>
let count = 0;
$: doubled = count * 2;
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
<p>Count doubled: {doubled}</p>
By declaring doubled
as a reactive declaration ($: doubled = count * 2
), it will be automatically updated whenever count
changes. This behavior simplifies managing derived states and ensures the UI stays in sync with the underlying data.
Reactive Statements
Sometimes, you may want to run statements or blocks of code whenever any variables referenced within them change. This is where reactive statements come into the picture. Let's look at an example below:
<script>
let count = 0;
$: console.log(`Current count is ${count}`)
$: {
console.log(`Fun fact: ${count} is the number that comes after ${count - 1}`)
}
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
Current count is {count}
</button>
In this example, we use reactive statements to log messages to the console whenever the value of count
changes.
You can also use control flow constructs like if
blocks and switch
blocks within reactive statements:
<script>
let count = 0;
let text = "";
$: switch(true) {
case count === 0:
text = "Click the button above ☝️"
break;
case count >= 1 && count < 5:
text = "Keep clicking"
break;
case count === 5:
text = "Count reached 5!"
break;
}
$: if (count > 5) {
text = "Okay, you can stop clicking now..."
}
function increment() {
count += 1;
}
</script>
<button on:click={increment}>
Current count is {count}
</button>
<p>{text}</p>
Props
In addition to its reactivity, Svelte offers mechanisms for managing the state through props. Props provide a flexible way to pass data from a component to its children.
Consider a scenario where you have a parent component that needs to display and update a name
to show a personalized greeting. You
Consider a scenario where you have a parent component that displays and updates a name
via its children. You can pass the data needed from the parent to its children through props:
<!-- App.svelte -->
<script>
import DisplayName from "./DisplayName.svelte"
import ChangeName from "./ChangeName.svelte"
let name = "John"
let names = ["John", "Ralph", "Alice", "Bob", "Sasha", "Emily"]
function changeName() {
const index = names.indexOf(name);
const nextIndex = index === names.length - 1 ? 0 : index + 1;
name = names[nextIndex];
}
</script>
<ChangeName {changeName} />
<DisplayName {name} />
In this snippet:
The
name
variable is passed to theDisplayName
component using the{prop}
syntax (shorthand forprop={prop}
).We also pass the
changeName
function into theChangeName
component to update thename
variable.
Next, we need to declare the name
and changeName
props in their respective components using the export
keyword in order to use them:
<!-- ChangeName.svelte -->
<script>
export let changeName;
</script>
<button on:click={changeName}>Change name</button>
<!-- DisplayName.svelte -->
<script>
export let name;
</script>
<p>Hello, {name}!</p>
Next, let's take a look at events in Svelte.
Events
Events in Svelte are crucial in facilitating communication between components. Svelte provides various techniques for handling events, including component events, event forwarding, and DOM event forwarding.
Component Events
Component events allow child components to communicate with their parent components by dispatching custom events. This feature enables child components to notify their parent components about specific actions or changes.
Consider a scenario where a child component emits an event to notify its parent component about a button click:
<!-- ChildComponent.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function handleClick() {
dispatch('message', { message: 'Button clicked!' });
}
</script>
<button on:click={handleClick}>Click me</button>
<!-- App.svelte -->
<script>
import ChildComponent from "./ChildComponent.svelte";
function handleButtonClick(event) {
alert(event.detail.message);
}
</script>
<ChildComponent on:message={handleButtonClick} />
In this example:
ChildComponent.svelte:
This component uses Svelte's
createEventDispatcher
to create an event dispatcher.The
handleClick
function dispatches a custom event namedmessage
with a payload containing the message "Button clicked!" when the button is clicked.
App.svelte:
The parent component imports and includes the
ChildComponent
.It defines a
handleButtonClick
function that alerts the message from the event's detail when the custommessage
event is received.The
ChildComponent
is wired to thehandleButtonClick
function via theon:message
directive, ensuring that the parent component listens for themessage
event emitted by the child component.
Event Forwarding
Event forwarding involves passing events from one component to another within the component hierarchy. It also allows intermediate components to intercept and modify events before forwarding them to their intended destination, providing a way to manage state and coordinate actions across multiple components.
Let's explore an example where a parent component contains an intermediate component, which in turn forwards events to the appropriate child component:
<!-- App.svelte -->
<script>
import IntermediateComponent from './IntermediateComponent.svelte';
function handleCustomEvent(event) {
alert(`Custom event received: ${event.detail.text}`);
}
</script>
<IntermediateComponent on:custom-event={handleCustomEvent} />
<!-- IntermediateComponent.svelte -->
<script>
import TargetComponent from "./TargetComponent.svelte";
</script>
<TargetComponent on:custom-event />
<!-- TargetComponent.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function triggerEvent(event) {
dispatch('custom-event', {
text: 'Locked In!'
});
}
</script>
<button on:click={triggerEvent}>Trigger Event</button>
In this example:
TargetComponent.svelte: This component is responsible for dispatching a custom event. When clicked, the button triggers the
triggerEvent
function, which uses Svelte'screateEventDispatcher
to dispatch acustom-event
.IntermediateComponent.svelte: This component forwards the
custom-event
from theTargetComponent
to its parent. It achieves this by simply includingon:custom-event
on theTargetComponent
. This syntax ensures that anycustom-event
emitted byTargetComponent
is re-emitted byIntermediateComponent
.App.svelte: The parent component listens for the
custom-event
emitted by theIntermediateComponent
. When the event is received, it triggers thehandleCustomEvent
function, which displays an alert with the event's details.
DOM Event Forwarding
DOM event forwarding involves forwarding DOM events directly to parent components for handling. This feature is useful when handling browser events, such as click
, mouseover
, or keydown
at a higher level in the component hierarchy.
Consider a scenario where a parent component listens for mouseover events on its child components to display additional information:
<!-- App.svelte -->
<script>
import ChildComponent from "./ChildComponent.svelte";
let text = "N/A";
function handleMouseOver(event) {
text = event.target.innerText;
}
</script>
<ChildComponent on:mouseover={handleMouseOver}>Component 1</ChildComponent>
<ChildComponent on:mouseover={handleMouseOver}>Component 2</ChildComponent>
<ChildComponent on:mouseover={handleMouseOver}>Component 3</ChildComponent>
<br />
<p>Last component mouse was over: '{text}'</p>
<!-- ChildComponent.svelte -->
<div on:mouseover>
<span><slot /></span>
</div>
<style>
div {
font-size: 20px;
cursor: pointer;
background: gray;
margin: 5px 0;
}
</style>
In this example, the ParentComponent
listens for mouseover events on its child components using DOM event forwarding. When a mouseover event occurs on a child component, the ParentComponent
handles the event and logs the text content of the child component to the console.
Bindings
Bindings in Svelte establish a connection between data in your JavaScript logic and elements in the DOM, allowing changes in one to automatically reflect in the other.
Bindings are particularly useful for form inputs and interactive elements to keep the JavaScript data and the DOM in sync. Svelte provides the bind:property
directive to establish two-way bindings between variables and DOM elements.
Consider a simple example where a text input field is bound to a name
:
<script>
let name = '';
</script>
<input type="text" bind:value={name} />
<p>Hello, {name}!</p>
In the above snippet, any changes made to the text input field will automatically update the name
variable, and changes to the name
variable are immediately reflected in the input field and the paragraph element.
We can take this further by passing input from one component to another by binding values to component props. Let's look at an example below:
<!-- App.svelte -->
<script>
import Greeting from "./Greeting.svelte";
import Input from "./Input.svelte";
let name = "";
</script>
<Input bind:value={name} />
<Greeting {name} />
In the above code:
We bind the
name
variable to theInput
component through itsvalue
prop.We display the
name
variable in the form of a greeting via theGreeting
component.
Next, we need to export the value
prop in the Input
component and bind it to the <input>
element:
<!-- Input.svelte -->
<script>
export let value;
</script>
<input type="text" bind:value />
And with that, we can now display the name
in the Greeting
component:
<!-- Greeting.svelte -->
<script>
export let name;
</script>
<p>Hello, {name}!</p>
It's worth remembering that overusing component bindings can make it difficult to track data flow within your application. Therefore, it's best practice to use component bindings sparingly.
Stores
Stores in Svelte serve as tools for managing state across your application. At their core, stores are nothing more than JavaScript objects equipped with a subscribe
method. This method allows interested components or parts of your application to receive notifications whenever the value of the store changes.
Let's explore the various types of stores and how to use them effectively.
Writable Stores
A writable store is a store that allows its subscribers to read from and write to its value. In addition to having a subscribe
method, a writable store also has a set
and an update
method for modifying its value.
Consider a scenario where you need to manage a user's authentication state across multiple components in your application. You can use a writable store to store the user's authentication status:
// stores.js
import { writable, derived } from 'svelte/store';
export const isAuthenticated = writable(false);
In this example, you have a stores.js
module where you defined a writable store named isAuthenticated
with a default value of false
.
In order to use the store in your App
properly, you need to:
Subscribe to the store to access its data.
Unsubscribe to the store if the component gets destroyed to prevent a memory leak.
Modify its value depending on the user's authentication state.
We can do this using the store methods (e.g. subscribe
and set
) provided to us:
<!-- App.svelte -->
<script>
import { onDestroy } from 'svelte';
import { isAuthenticated } from './stores.js';
...
let auth;
const unsubscribe = isAuthenticated.subscribe(value => {
auth = value;
});
function submit() {
// validation logic
// ...
isAuthenticated.set(true)
}
onDestroy(unsubscribe);
</script>
...
{#if auth}
...
{:else}
...
{/if}
However, this method involves setting up a lot of boilerplate code.
This is where auto-subscriptions come into play. By prefixing the store name with $
, Svelte will automatically subscribe and unsubscribe to the store for you:
<!-- App.svelte -->
<script>
import { isAuthenticated } from './stores.js';
...
function submit() {
// validation logic
// ...
$isAuthenticated = true
}
</script>
...
{#if $isAuthenticated}
...
{:else}
...
{/if}
You can also modify the store value ($isAuthenticated = true
) without having to use the set
or update
method.
Next, let's finish up the App
to see the store in action:
<!-- App.svelte -->
<script>
import { isAuthenticated } from "./stores.js"
let username = "";
function submit() {
// validation logic
// ...
$isAuthenticated = true
}
</script>
<nav>
<h2>Theme Checker</h2>
</nav>
{#if $isAuthenticated}
<p>Hi {username},</p>
<p>The current theme is {window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'}!</p>
{:else}
<form on:submit|preventDefault={submit}>
<label for="username">Username</label>
<input
type="text"
id="username"
name="username"
bind:value={username}
required
/>
<label for="password">Password</label>
<input type="password" id="password" name="password" autocomplete="off" required />
<button type="submit">Login</button>
</form>
{/if}
Readable Stores
A readable store allows its subscribers to read its value but not modify or write to it. Unlike a writable store, a readable store doesn't expose its set
or update
methods to its subscribers.
From our previous example, we have an app that displays the current theme after logging in. Let's say we want to:
Create a store for the current theme so its value is available to other components.
Update the theme value whenever the user changes their color scheme (from their browser or operating system).
In this case, the components referencing the theme don't need to modify the value. We can simply update the value from 'inside' by using a readable store:
// stores.js
import { writable, readable } from 'svelte/store';
...
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
export const theme = readable(
darkModeMediaQuery.matches ? 'dark' : 'light',
(set) => {
function setColorScheme(event) {
const newColorScheme = event.matches ? 'dark' : 'light';
set(newColorScheme);
}
darkModeMediaQuery.addEventListener('change', setColorScheme);
return () => {
darkModeMediaQuery.removeEventListener('change', setColorScheme);
};
}
);
In this example:
We create a
MediaQueryList
objectdarkModeMediaQuery
representing the result of the media query(prefers-color-scheme: dark)
.We set the initial state of the theme using
darkModeMediaQuery.matches ? 'dark' : 'light'
.We add a second argument to the readable method, which exposes the
set
method.We add a listener to
darkModeMediaQuery
to track changes in preference. When the preference changes, the listener callback function is called with an event containing the new state, and we set the new color scheme.
Next, let's display the current theme
in our App
:
<script>
import { isAuthenticated, theme } from "./stores.js"
let username = "";
function submit() {
// validation logic
// ...
$isAuthenticated = true
}
</script>
...
<p>Hi {username},</p>
<p>The current theme is {$theme}!</p>
...
We should now have a working theme status checker!
Derived Stores
As the name implies, derived stores are stores whose values depend on or are gotten from one or more existing stores. Derived stores allow you to compute and expose derived values based on the values of other stores.
In our theme checker, suppose we want to:
Manually change the current theme using a toggle switch.
Style our theme checker app directly using
--style
directives.
To implement the first task, we first need to update the theme
store to a writable store so we can modify its value from 'outside':
// stores.js
import { writable, derived } from 'svelte/store';
...
export const theme = writable(
...
);
Next, for styling our app, we can create an object for our styles:
...
const styles = {
light: {
background: '#ffffff',
button: '#f9f9f9',
slider: '#f2f7fd',
toggle: '#a6a6a6',
text: '#213547',
},
dark: {
background: '#242424',
button: '#1a1a1a',
slider: '#383838',
toggle: '#737373',
text: 'rgba(255, 255, 255, 0.87)',
},
};
The object contains all the styles for our UI elements for the light and dark themes. Since we only need the styles for the current theme at any point in time, we can create a derived store to store only the current theme styles:
...
export const themeStyles = derived(theme, ($theme) => styles[$theme]);
The derived
function, in its simplest form, works by taking two arguments:
The first is a single store we want to derive our store value from, which is the
theme
store in our case.The second argument is a callback function that returns the derived value (
styles[$theme]
).
With the stores in place, let's apply the necessary changes in the app to get our toggle switch and new styling:
<!-- App.svelte -->
<script>
import { isAuthenticated, theme, themeStyles } from "./stores.js";
...
function onCheckboxChange(event) {
let checked = event.target.checked;
$theme = checked ? "dark" : "light";
}
$: checked = $theme === "dark";
$: ({ background, button, slider, toggle, text } = $themeStyles);
</script>
<div class="container" style:--background={background} style:--text={text}>
<nav>
<h2>Theme Checker</h2>
{#if $isAuthenticated}
<label class="switch">
<input
type="checkbox"
bind:checked
on:change={onCheckboxChange}
/>
<span
class="slider"
style:--slider={slider}
style:--toggle={toggle}
></span>
</label>
{/if}
</nav>
{#if $isAuthenticated}
...
<p>The current theme is {$theme}!</p>
{:else}
...
<button type="submit" style:--button={button}>Login</button>
...
{/if}
</div>
Custom Stores
Custom stores provide flexibility in creating stores with custom logic. You can define custom stores by creating an object with the subscribe
method, along with any additional methods or properties you need.
Suppose we want to implement a global notification system in our application. We can use a custom store to manage the notification state and provide methods for adding and removing notifications:
// stores.js
import { writable, derived } from 'svelte/store';
...
function createNotifications() {
const { subscribe, set, update } = writable([]);
return {
subscribe,
addNotification: (payload) =>
update((notifications) => [...notifications, payload]),
removeNotification: (payload) =>
update((notifications) =>
notifications.filter((notification) => notification.id !== payload)
),
clearNotifications: () => {
set([]);
},
};
}
export const notifications = createNotifications();
The createNotifications
function returns a custom store with methods for adding, removing, and clearing notifications.
Next, let's import the function into our app and utilize its methods to manage notifications:
<!-- App.svelte -->
<script>
import {
isAuthenticated,
theme,
themeStyles,
notifications,
} from "./stores.js";
...
function submit() {
// validation logic
// ...
$isAuthenticated = true;
notifications.addNotification({
id: new Date().getTime(),
content: `User logged in at ${new Date().toLocaleTimeString()}!`,
});
}
function onCheckboxChange(event) {
...
notifications.addNotification({
id: new Date().getTime(),
content: `Changed theme to ${$theme} at ${new Date().toLocaleTimeString()}!`,
});
}
...
</script>
...
{#if $isAuthenticated}
<p>Hi {username},</p>
<p>The current theme is {$theme}!</p>
<br />
<h4>
Notifications
<button
on:click={() => notifications.clearNotifications()}
style:margin-left={"10px"}
style:--button={button}
>
Clear
</button>
</h4>
<ul>
{#each $notifications as notification}
<li>
<div>
<span>{notification.content}</span>
<button
style:margin-left={"10px"}
style:--button={button}
on:click={() =>
notifications.removeNotification(
notification.id
)}
>
Remove
</button>
</div>
</li>
<br />
{/each}
</ul>
...
In the above code, the notifications
store is used to manage a list of notifications. Notifications are added when the user logs in and when the theme is changed. Users can also clear all notifications or remove individual ones.
Context API
The Context API in Svelte allows you to pass data deeply through the component tree without having to pass props at every level. This is particularly useful for shared state or services.
Suppose you have an application where multiple child components need access to a user profile and a messaging service. You can use the Context API to provide these shared resources.
Firstly, let's define two stores for the user profile and messaging service in a services.js
module:
// services.js
import { writable } from 'svelte/store';
export const userProfile = writable({
name: 'John Doe',
email: 'john.doe@example.com',
});
export function sendMessage(message) {
alert(`Message sent: ${message}`);
}
Next, we can access the stores in the parent component by resources using the setContext
function:
<!-- App.svelte -->
<script>
import { setContext } from 'svelte';
import Child from './Child.svelte';
import { userProfile, sendMessage } from './services.js';
setContext('userProfile', userProfile);
setContext('sendMessage', sendMessage);
</script>
<Child />
To access this context in the child component, we can use the getContext
function:
<!-- Child.svelte -->
<script>
import { getContext, onMount } from 'svelte';
const userProfile = getContext('userProfile');
const sendMessage = getContext('sendMessage');
let message = '';
function handleSend() {
sendMessage(message);
message = '';
}
</script>
<p>User: {$userProfile.name}</p>
<p>Email: {$userProfile.email}</p>
<input type="text" bind:value={message} placeholder="Type a message" />
<button on:click={handleSend}>Send</button>
Components can also update the shared state gotten via context. Let's extend our example to allow the child component to update the user's email:
<!-- Child.svelte -->
<script>
...
let message = '';
let newEmail = '';
function handleSend() {
sendMessage(message);
message = '';
}
function updateEmail() {
$userProfile = { ...$userProfile, email: newEmail };
newEmail = '';
}
</script>
...
<input type="text" bind:value={message} placeholder="Type a message" />
<button on:click={handleSend}>Send</button>
<input type="email" bind:value={newEmail} placeholder="Update email" />
<button on:click={updateEmail}>Update Email</button>
In this extended example, the child component can update the user's email using the update
method on the userProfile
store.
Module Context
Module context in Svelte allows you to define code that runs once per module, regardless of how many times the component is instantiated. This is useful for initializing shared resources or performing setup tasks that should only happen once.
Let's consider a simpler example where we need to initialize a counter that increments every time a new instance of a component is created. This counter should maintain a global count across all instances of the component.
First, set up the global counter in the module context:
<!-- Component.svelte -->
<script context="module">
let globalCounter = 0;
export function incrementGlobalCounter() {
globalCounter += 1;
return globalCounter;
}
</script>
<script>
import { incrementGlobalCounter } from './YourComponent.svelte';
let instanceCounter = incrementGlobalCounter();
</script>
<p>This is instance number {instanceCounter} of this component.</p>
The module context block defines a global globalCounter
variable and an incrementGlobalCounter
function that increments and returns the counter value. This ensures that every time the component is instantiated, it gets a unique instance number.
Now, whenever the component is instantiated, it will display its unique instance number based on the global counter:
<!-- App.svelte -->
<script>
import Component from './Component.svelte';
</script>
<Component />
<Component />
<Component />
Conclusion
In this guide, we've explored the key aspects of state management in Svelte and how you can effectively use them in your applications. By mastering these concepts, you can ensure that your application state is predictable and easy to manage.
For further reading on any of the concepts outlined in this article, you can go through the Svelte documentation.