The Power of Classes for Reactivity
I cannot believe that classes, yes, JavaScript classes are one of the best ways to handle reactivity in the modern web. In my last video, I was talking about a lot of the big issues Svelte has in 2025. And one of the big issues is that there is only one component per file.
But this problem has been massively alleviated because of the new runes primitives and how those fit with stores and classes and how you can put all these together to make some really, really cool stuff. So, in this video, I wanted to go over a couple examples of how that works, show you what the mental model for this looks like, and all of the examples and stuff I go over here is fully open source. It'll be linked down below. And yeah, let's get into it.
The Old Way: One Component Per File
So, Svelte's classes and stores. I think the best place to start here is with how this actually works without our classes and stores. For the entirety of this video, I'm just going to be assuming we're working in SvelteKit. You can use Svelte without SvelteKit, but frankly, I don't think it's very good. If you wanted to do SPA stuff, SvelteKit has that built in. If you want to do SSR, server side type stuff, SvelteKit also has that built in. It just feels like the best, most mature, well put together way to work with Svelte in 2025. So, we're going to be talking about that.
So oftentimes what will end up happening is we'll have our page.svelte here. Uh within the page.svelte, we'll have a Svelte component. And these Svelte components are composed of a script, the markup, and the styles. All three of these coexist together within the page.svelte, but we can only have one of each of these. It's the single component per file problem. And this means that we have to put all of our logic in the script. We have to put all of our markup in here. And if we wanted to split these things out, we're going to have to end up using another component, which means another file.
So we could take this here. We could duplicate this over here. We could call this um todos.svelte. And then we can import this new component up in our script tag and then mount this down within our original markup. This system is fine. It's not the most ergonomic thing ever. I don't particularly love it, but this is generally how you had to do things in Svelte 4 before the new runes primitives.
The New Way: Runes and Reactive Classes
But Svelte 5 introduced the runes. We have our $state rune, our $effect rune, our $derived rune, the $inspect rune, and a couple more, but these are the key important ones. And what's really cool about these is we can obviously use these in .svelte files within the script to handle our reactivity, but we can also use these in .svelte.ts files to create stores. And that's what I wanted to talk about here.
So let's take our original example up here. Let's grab this guy. Let's copy this and paste it down here. And let's say for this example, we wanted to abstract out a lot of the logic within our script. This is a surprisingly common thing I want to do. I like to have my scripts be simpler to understand, and especially when you're working with something more complicated like D3 or you're doing some backend logic to fetch data or something like that, it can get really big, really bloated and kind of hard to reason about and work with. So what we can do is we can create a new file over here and we will call this uh todos-state.svelte.ts.
And what we can do with this todos-state.svelte.ts is we can put all of the logic for working with our to-dos in here using a class. So within this todos.svelte.ts, I'm going to create a class in here. So we'll call this TodoState. This will be a class. And within this class, we're going to have a couple of different fields. We're going to have all of our todos. We're going to have addTodo. And with this new todo-state.svelte.ts, we can import this up into our script tag up here. And then we can create a new TodoState class within our script and then use the todos method and the addTodos method down within our markup while having full reactivity because of the runes.
Example: A Component-Local Chat Store
Like I said, I made a full example for this. It's linked down below, fully open source if you want to see the source code for how I actually do this stuff. And the first example I made here is a basic little chat app. So we have this right here. I can put some text in and we'll get a random fake response back. None of this is, it's not doing any server side stuff. It's just to illustrate how the state actually works.
And the code for this is incredibly simple. We go into our store here and you'll see we are importing our ChatState class. We're creating a new ChatState right here. And then within our actual markup here, we're grabbing our messages off of chatState and we're grabbing the sendMessage off of chatState. It's all encapsulated into this nice little class box here.
And then if we go into our chat-state.svelte.ts, the way I actually implement this is with a class. The way I like to start these things is with an interface. I'll just define, okay, these are the things I want this to have. I want this to have a messages field, I want this to have an isLoading field, and I want this to have a sendMessage field. Then we can go ahead create the class that implements our interface.
And the first two things I do is I create my messages and I create my loading. And what's really cool is we're using the $state rune here, which means that these messages and this isLoading are normal reactive state variables just like if we had initialized these within the script tag of our page.svelte. Then down here I have my fake responses. I have my sendMessage which will mutate all those state variables. So we'll set isLoading to be true. We'll push to our messages, and then after 400 milliseconds, we'll push the fake assistant response and then set isLoading to be false. This makes encapsulating and reasoning about client side logic so much easier, especially when I'm working with something more complex like a D3 type thing or if I have to do a lot of complex back and forth with a server or if we wanted to share the state between components, we can because we're creating stores.
The Problem with Sharing Local State
Now, in this example, we are initializing the store within our actual script tag. If we go back over to our diagram here and look at sort of how this actually works, we can imagine we have the Svelte tree. So let's say we have a normal little Svelte app here. Let's say we have up here, make this dash, we have our +layout.svelte and then we have two pages on this. We have one page.svelte and then we have another page.svelte.
And in the example I just provided there where we have our state here, we'll just use the TodoState example. We're actually initializing this TodoState within this page.svelte. So, this guy is getting put inside of here. So, if we wanted to access the TodoState in this page or within this layout, we can't because all of that logic is being encapsulated within this one page. That's what's happening here because we're creating the instance of that ChatState within this component. We can't share that between components.
But if we wanted to share these things, we could go ahead and create an actual store for these things. So, we'll just call this over here, um, store. We'll have this store over here which is an instance of this TodosState class and then we can access this within any of these components and they will be tied to the same thing. So whenever we mutate the state within one of these page.sveltes it will be mutated on the store which is shared in between them using context.
Example: A Shared Counter with Context
And the way this looks is with this example. I have this little example here where if we increment this counter, all of these counters get incremented together. I go to the subpage and that state is shared between them. We'll decrement this a bunch. Go back to the context and they're all decremented together.
The code for this is very similar to the other. We create our interface. We create our CounterState class. We have our reactive count variable, our increment, and our decrement. But what's different is down here we're creating these two functions called getCounterState and setCounterState. Effectively what these are doing is the setCounterState is creating the context or creating the store which is shared. The important thing to remember here is that Svelte works like a tree. The layout is the parent of the pages and then the pages are the parent of the components which they import.
So going back to our diagram here, when we're creating our store, we actually want to be creating the store within our layout.svelte and then we get our store within these two pages. So we're going to call setCounterState within the layout, then getCounterState within the actual pages or components to work with it. So if we go into our layout.svelte here, we're setting our counter state. Then if we go down into our page here, we're just importing a bunch of these counter components. And if we look at our counter component here, we are calling getCounterState to get access to that class. And these are all tied together because they're all tied to the same context.
A Critical Warning for SSR
And the way context works is it's keyed. If we wanted to create multiple of these counter stores within the same tree that are not tied together, we could provide them with uh different keys. In this case, I'm just setting them all to be the default key of this like $counter-state whatever thing. So in this case, whenever we call getCounterState with no arguments, we're getting this one. When we setCounterState, we're setting this one. So down here within our counter state, it works exactly the same as the initial example did. We have our increment, we have our decrement, and we have our actual count right here. It's all reactive. It all just works, and it's really, really nice.
If you're doing anything with SSR, it is really important that you don't just create a new CounterState class within this and export it and then import it between those. That can create really weird leaking with server-side rendering that you don't want to deal with. Make sure if you want to share these between components, you are doing it using context.
Conclusion: A Superior Mental Model
And that's it. That's all you have to do to create a fully reactive store in Svelte 5. That is much richer than what you were previously able to do with the readable and writable syntax. This is very similar to the React context API except it's much much nicer to work with. This is an incredibly useful thing to have.
I'm working on a project right now where one of the things I want to have is a global toast. And what we can do is we create this toast component that is tied to our toast store. Within our toast store, we have an open function, a closed function, and then anywhere within our app because within our app's tree here, within our root layout right here, um we're setting our toast store, which means that we now have access to it anywhere within our app on any of these components. I can just call getToastStore and then toastStore.open and it will open up that store because we're mounting the toast component within our layout. You can do crazy things with these and it is uh it just feels so good to work with.
Once you get the mental model of this down, it's hard to go back to working with anything else. I have not wanted to touch React entirely because of these. This has solved Svelte for me and I highly recommend playing with these things. Like I said, the example is open source. It's linked down below. If you enjoyed this little breakdown on how these things work, make sure you like and subscribe. I'll have more Svelte content coming soon as well as a bunch of weird effect stuff and random things I'm working on. And yeah, until next time, write more Svelte.