|
Let’s face it - If you’re like me and have battled the complexities of Redux or gotten lost in React’s Context labyrinth, you know it’s no walk in the park. But here’s some good news: SvelteKit is like that cool friend who just makes things easy. And not just easy—elegant, too. In this article, we’re going to explore why SvelteKit’s approach to state management feels like a breath of fresh air, especially when compared to React’s often convoluted systems.
Here’s the magic equation: when things are simple, you intuitively know what to do, and that means you build stuff fast. Crazy fast. That’s SvelteKit in a nutshell. React is like that old, reliable car you’ve had for years. It gets you where you need to go, but oh boy, the maintenance! You find yourself tinkering under the hood more often than actually driving. In contrast, SvelteKit feels like hopping into a sleek, self-driving Tesla. You just set your destination and enjoy the ride.
In Svelte, the joy begins with the beautifully simple syntax. There’s no JSX to wrestle with or “useState” and “useEffect” hooks to manage your component’s lifecycle. You’re working with something that feels like HTML, CSS, and JavaScript got together and had a lovely, well-behaved child. This simplicity naturally leads to intuition. You find yourself guessing the right way to do things, and surprisingly, you’re often correct! You know that feeling when you’re using a new app and you think, “I bet this button does X” — and it does? That’s SvelteKit for you. Everything is where you expect it to be.
So, let’s talk about everyone’s favorite subject - state management!
When I was first learning Svelte, I was shocked at how simple it makes state management. In a React environment, the complexities of state management quickly escalate, often pushing developers toward cumbersome solutions like Redux or MobX. SvelteKit, however, brilliantly simplifies this process.
Imagine a world where you don’t have to call a function to change a variable and trigger a re-render. In Svelte, and by extension SvelteKit, all you have to do is update a variable—yes, a good old JavaScript variable—and the UI magically updates. It’s as simple as prepending a variable with $: and letting Svelte do the rest2
.
There’s no need to “lift state up” or drill props down multiple component layers. Your code remains clean, readable, and intuitive. When a variable changes, your UI changes. Period. You spend less time tracking state changes and more time crafting the user experiences you actually care about. It’s like having a helpful assistant who instinctively knows when to refresh your coffee—except this one updates your UI.
Pictures (or I guess code snippets in this case) are worth a thousand words, so let’s take a look at one:
import {writable} from 'svelte/store';
export const count = writable(0);
Simple, right? We’ve just created a writable store, initialized at zero. No Redux boilerplate or elaborate setup; it’s just JavaScript.
Now, let’s integrate this store into a SvelteKit page. Here’s where the magic happens. In our page, we import the count store, then perform increments and decrements directly within the component.
<script lang="ts">
import { count } from '$lib/counters';
function incrementCount() {
$count = $count + 1;
}
function decrementCount() {
$count = $count - 1;
}
</script>
<div>
<div class="flex flex-col bg-orange-200 p-4 text-black">
<p>Count in the page:</p>
<p>{$count}</p>
<div class="flex flex-row gap-2">
<button
on:click={decrementCount}
class="rounded-lg bg-gray-800 px-4 py-1 text-white"
>-</button
>
<button
on:click={incrementCount}
class="rounded-lg bg-gray-800 px-4 py-1 text-white"
>+</button
>
</div>
</div>
</div>
The beauty? When count changes, it’s instantly reflected in the UI. Have a look:
0
But hold on, it gets better. Let’s say you have a separate Svelte component called CounterThing where you want to use the same value. Thanks to SvelteKit, it’s a breeze. Have a look at this:
<script lang="ts">
import { count } from '$lib/counters';
function increment() {
$count = $count + 1;
}
function decrement() {
$count = $count - 1;
}
</script>
<p>{$count}</p>
<div class="flex flex-row gap-2">
<button on:click={decrement} class="rounded-lg bg-gray-800 px-4 py-1 text-white">-</button>
<button on:click={increment} class="rounded-lg bg-gray-800 px-4 py-1 text-white">+</button>
</div>
On its own, this new component is almost identical to what we added to the page earlier. Here it is surrounded by everyone’s favorite red css outline:
0
What makes it cool, however, is the reuse of the $count store. The value of $count is instantly available and in sync across all components that import it. No prop drilling, no context providers, just straightforward and readable code.
Here’s the real kicker, though. If you click the +/- buttons in the component, you’ll notice tha the value of count earlier on the page also changes. Simply because they use the same store.
Take a second to process that. SvelteKit doesn’t just make coding easier; it makes it downright enjoyable. You spend less time wrestling with state management and more time building, well, awesome stuff!
“But wait!” - I hear you saying - “What if I want a different counter? Or a different pair of counters? Aren’t these store objects kinda global by nature? What if I want to scope them to some specific component tree like React Contexts?
Well, my delightfully well-informed and curious friend, you’re in luck.
In Svelte, we use Contexts to scope data to componenttrees. Let’s use our counter thing as an example.
Right now, if we add a bunch more CounterThings to the page, they’ll all use the same value. So if I add two different groups of two counters each…
P.s. this whole website is written using SvelteKit. And it’s open source! Check it out on my github.
<script lang="ts">
import { CounterGroup } from '$lib/counters';
</script>
<div class="flex flex-row w-screen">
<CounterGroup groupNumber={1} color="green" />
<CounterGroup groupNumber={2} color="blue" />
</div>
<script lang="ts">
import CounterThing from './CounterThing.svelte';
export let color: 'green' | 'blue' = 'green';
$: bgColor = color == 'green' ? 'bg-emerald-400' : 'bg-sky-400';
export let groupNumber = 1;
</script>
<div class="mx-auto flex grow flex-col items-center p-4 {bgColor}">
<p class="font-mono text-xl font-bold">Counter Group {groupNumber}</p>
<CounterThing />
<CounterThing />
</div>
What we’ve done here is create a new component - CounterGroup - that contains two CounterThings. CounterGroup also has a few props used to stylize their background color and their label value. The page now contains just two of the new CounterGroup components.
Now if you press the ‘+’ button on any of the counters, then all of them will update!
Counter Group 1
0
0
Counter Group 2
0
0
Here’s where Svelte’s Context API comes in handy. The Context API allows us to create named ‘Contexts’ that are only available to child components.
import { getContext, setContext } from 'svelte';
import { type Writable, writable } from 'svelte/store';
export function getCount(): Writable<number> {
return getContext('count');
}
export function setCount() {
let count = writable(0);
setContext('count', count);
}
Think of a ‘Context’ as a plain-old javascript object that maps a key (in this case counter) to a value (in this case the count writable). The cool thing about the Counter is that they’re auto-magically scoped to the parent component’s context. I.e. only the parent component (whoever calls setCount) and the children components (as defined by the HTML structure of your app) have access to the values defined in the context.
To add our new context to our components, all we need to do is call setCount in the CounterGroup, and then we can call getCount in CounterThing to.. Uh.. get the count store.
<script lang="ts">
import CounterThing from './CounterThing.svelte';
import { setCount } from './counterContext';
setCount();
export let color: 'green' | 'blue' = 'green';
$: bgColor = color == 'green' ? 'bg-emerald-400' : 'bg-sky-400';
export let groupNumber = 1;
</script>
<div class="mx-auto flex grow flex-col items-center p-4 {bgColor}">
<p class="font-mono text-xl font-bold">Counter Group {groupNumber}</p>
<CounterThing />
<CounterThing />
</div>
<script lang="ts">
import { getCount } from './counterContext';
let count = getCount();
function increment() {
$count = $count + 1;
}
function decrement() {
$count = $count - 1;
}
</script>
<p>{$count}</p>
<div class="flex flex-row gap-2">
<button on:click={decrement} class="rounded-lg bg-gray-800 px-4 py-1 text-white">-</button>
<button on:click={increment} class="rounded-lg bg-gray-800 px-4 py-1 text-white">+</button>
</div>
And voila! It’s that easy!
Counter Group 1
0
0
Counter Group 2
0
0
There is one caveat here, however. On their own, Contexts are not reactive. You may have noticed that I used a writable in the context rather than just an int. This is because if I had used an int, the child components would not have simply re-rendered themselves.
Note, this isn’t just some quirk. It actually has really useful implications. For example, if I need to provide child components with some data that is only available at runtime, I can simply stick the data into a context, and they will display the data that is provided. That can be super useful when dealing with complex data that doesn’t need to change reactively.
In Svelte, Stores and Contexts are all you need to worry about to manage even complex state. Stores are the containers for reactive values that are just plain old JS/TS, and Contexts allow you to scope data (including Stores) to specific component trees.
If you’re familiar with React, you already know how much simpler this set up is. Svelte’s concept of a Store eliminates the need for both useState and useEffect, and the Context lets you silo data in the same way that React’s Providers do — just with way less boiler plate. Since everything is a plain old JS/TS object, you can do whatever you want with it. The world is your freaking oyster.