Svelte 5 Preview: Runes, Signals, and a New Approach
So unless you've been living under a rock for the past couple of days, then you're aware that this Svelte 5 preview was announced. They dropped a REPL, a blog post, and a video with the announcement and overall the feedback has been pretty good. Like, I feel like the overall sentiment about it within the greater community has been good. There has been some concerns about the syntax, you know, it's not being as nice as it once was, but putting that to the side, like looking at the overall performance improvements, the JS output, the fact that we're moving to signals for finer-grain reactivity is pretty awesome.
And so rather than going into like the differences and all the little things that change like the $state, the $effect, because I think all of those are pretty much subject to change. Again, this is still an early preview, so don't look at this as like this is what's going to be shipped to 5.0 production. This is being put out there at a very primitive level so they can collect feedback from the community and make changes and iterate, find bugs, things like that.
And so I think rather than going through all the different like Runes, what they do — Joy of Code made a great video on that, it's about eight minutes long, you can go check it out if you're completely unaware of what's going on with this new update. What I wanted to do today was kind of have like a scenario or have something above like the very minimal surface level of the APIs, right? And it's going to involve implementing some type of feature that's reusable across multiple different places in your application.
The "With Previous" Feature Demo & Requirements
And we're going to do so using custom stores, and then we're going to look at the same exact thing using the new signals and Rune syntax.
And so the scenario here is that like we have this component right now which has a previous, a current input value. Input value here isn't super important, it's just capturing the value of the input so we can update the value. But what we want to happen is when we update this current value, we want the previous value to take its previous value. So right now the current value is green. So if I change this to yellow, we're going to see that previous now has green and current has yellow. If I do blue, you're going to see that it shifts, and then when I click on undo, you're going to see that it resets previous to undefined and current to yellow. So we're not going like super far back in history. We're not keeping like a trail of like undos and redos, just to keep it somewhat simple for this video, but still not like so simple that it'll be hard for you to see the difference or to see the value.
And so that's the goal, is we want to create like some type of reusable function that can return a current, previous, and undo, and take any arbitrary state and essentially apply the same logic to it, right? Additionally, we don't want to be able to mutate the previous value like outside of that function. So like it should only ever be changed whenever the current is changed, right? We shouldn't be able to mutate it outside of that.
And so it might look something like this. If you're doing stores it might be like cur, prev, undo equals custom store initial value. So in this case green. And then if you're doing something with signals, it would look something like sig or whatever color, just call it whatever, and then custom signals, something like that, right? And so if you want to go ahead and try to do that yourself and see how you would implement something like this, feel free. Otherwise, let's go ahead and dive into it, starting with the stores.
Building a Svelte 3/4 Custom Store
And so I have this withPrevious.js file here that I'm going to go ahead and define both of these things in. And I know that one thing we're definitely going to need is writable and we're also going to need readonly. So we're going to need writable for our stores, and we'll need readonly because we want to make the prev store only read-only when it gets returned back so it can't be updated from outside of this function. And we'll talk about that here in just a second. But we'll import those from svelte/store and then I'm going to set up a function called withPreviousStore. It's going to take in an initial value, and what we're going to set up first is our cur store, so our current store. And we'll just say writable(initialValue). And then we'll have a prev store which will be writable inside of this function because we do have to update it, but when we return it, we're going to wrap it with this readonly so that it gets turned into like a read-only derived store. And this is going to be undefined by default.
Now, if you're not familiar with custom stores already, I made a video on it a few days ago which you can go check out, which will definitely help, I guess, connect the dots here if you've never written a custom store like this before. But we need to access the current value when it's being changed, right? And so to do that, we have to essentially write our own custom update function. So you know, you can call cur.update here for the store, it gives you the current value. Well, we actually need to intercept that so that we can set previous to the current value before it actually changes. So we'll define an update function which takes in an updater, which is that callback I just showed you right there. And then what we're going to do in here is we're going to say cur.update and we have the current value of cur before it changes. And then what we're going to do is we're just simply going to set previous to currentValue. And then what we'll do is we'll return updater, which is that callback, with the currentValue, just like that, right?
So now we have our update. Now, update is not enough though because if we use this cur store outside of this inside of a component and we do something like cur = whatever, then that's going to be a set, that's not an update. So we also need to have a custom set function as well. So we'll set up a function called set and it's just going to take in a new value and all we're going to do is we're just going to call the update function here and we're just going to pass in the new value. We'll return the new value from that function. So what's going to happen is it's going to pass through update, it's going to set the previous, and then it's going to call this callback here, which just returns the current value, which then would set current to that value.
And then for undo, what you want to have is we're going to have a function called undo and what we're going to do is we're going to do prev.update. We
get the prevValue and let me just add some more space here so I can scroll down a bit, and we're going to do a cur.set(prevValue). So we're taking whatever's inside of the previous value or the previous store right now, the value of the prev store and setting it to the current store, or setting the current store to that value. And then we're going to return undefined, which will set the prev to undefined.
Okay, and then we want to return these. So we'll have to return, we'll have a cur which will just spread cur to get the subscribe method, and then we'll do update and set. And then we'll also have to do the prev, which we're going to turn into a read-only store by just wrapping the store in this readonly function here. And then we'll also return undo.
And so this looks pretty good, right? So we're intercepting the update, we're intercepting the set, we're overriding them, customizing them, doing some things with them, and then we should be good to go now.
Using the Store and Critiquing its Ergonomics
So we should be able to import this into our App.svelte. Let me just remove some of these things here. We don't need this, we don't need this, and we don't need this. And so now we can just import withPreviousStore from withPrev. And then we'll set up our cur, prev, undo equals withPreviousStore and we'll pass in green again, right? And then everywhere that we're using like cur here, we're going to set this to $cur to access the value or to subscribe to that store. And then same thing with prev here. And then we can just leave everything else alone, right? And as you can see, when we type in yellow here, hit save, we're going to see that current was updated to yellow, previous updated to green. If we do blue, we're going to see that it still works. When we do undo, we're going to see that it does in fact work just as it did before, right?
But what's really cool is that we essentially have this cool like current, previous, undo that we could use for any arbitrary state across our app, right? And this works fine, this works great. What isn't like so great is like this isn't very intuitive. Now if you're familiar with stores already and you've already written a few custom stores, then this sure, this might seem intuitive to you. But you have to take your... take a step back and imagine you're someone who's just coming into the language for the first time, you're starting to see this and you're like, okay, what is really going on here? Why can't I just use like, you know $current in here? Like there's all these little things, right, that we have been writing the syntax for years now, so we are very familiar with it or most of us are, or some of us are I should say. So it feels natural, but it's really not that natural when you think about it.
Rebuilding with Svelte 5 Runes
And so let's compare this to the signals. So I'm just going to add some lines here to get some space for myself. And so we're just going to set up a new function called withPrevSignals I guess we should say. And we'll have initialValue. And we're going to let cur = $state(initialValue). let prev = $state(undefined) to begin with, right? Just as we did up with the stores.
And then we're going to return some stuff after we set up our undo function. So we'll have a function called undo, and what is undo going to do? It's going to set the current value to the previous value and then it's going to set previous to undefined. That's it right there. So it's like just even just comparing these two, like sure, it's not like an extra... it's an extra two lines of code or whatever but like what you have to know to accomplish this is different, right? This just makes sense. This looks like how we had it in our component when this video first began.
And so now we can return some stuff. We have to return a getCur so that we can actually get the latest cur, not the value of cur whenever we call this function, but whatever cur is at that point in time that we're trying to access it. And so we'll return cur. We also want a setCur. And we'll take in a value, and what we're going to do here is remember we're intercepting that update up here in the store, right? So we're doing like cur.update and then we're setting previous in there. Well, in here all we have to do is get this, say prev = cur because at this point in time we haven't changed cur yet, right? And then we can do cur = v just like that. And this is like the new value, so let me just change this to newValue so it's more explicit here.
And then remember we want prev to be read-only, so we can just return a getter for prev, not have a setter, that way it's only read-only. So we can do return prev, boom, and then do an undo here. So this is what it takes to implement the exact same logic without any imports whatsoever as this.
Now, some of you might be thinking, okay, why don't we just like do cur = writable, have some type of object here that has the current whatever and the prev. That's sure, that's possible, but is it like clean? I don't really... I don't think it's really clean. And then we also have to handle the case where like, remember we don't want prev to be mutable, so we have to make sure that when they update it, they're not accidentally overwriting prev. That adds a little bit of complexity here that is likely not ideal. And so this is really cool, right? This looks almost as exactly as we had it inside of our Svelte file when we started this video, right? Except you know, we have to have these gets here because we are crossing a function boundary. We're getting the value from outside of this function, right? So we need some type of way to get that, and that's what these are doing here.
Integrating the Rune-based Composable
And so now we can refactor this to actually use withPreviousSignal. So we'll say withPrevSignals is what I called it. And we can't destructure here because we have getters and setters, it just doesn't work like that. So let's just do color here.
And so now instead of doing $cur we'll do color.getCur. We'll do... color.getPrev, color.getCur, and then color.undo... and you're going to see here this works the exact same way. So if I do orange, okay it works. If I do green, it works. I can undo just fine.
And so that's pretty cool, right? Like, and we could have as many of these things in here as we wanted to. And you can imagine like if we had a few more of these things that this would become a lot messier, right? Especially for using like multiple current values. Like we have to keep essentially nesting these updates to make sure that we're getting the current value of that store at that point in time.
Final Thoughts on Composability and Svelte 5
And so like this is what I think is pretty awesome about this update. Like the composability — like sure, is it possible today? Absolutely. Is it ideal? Absolutely not.
So anyways, I hope this video has been informative for you all. Again, this is just one example. I hope that you were able to try the challenge out yourself and that you got some value out of this. If you want to see me do like a deeper dive on some of the runes and how you can use them, leave a comment down below. I'm more than happy to do so.
Um, and yeah, I'm really liking this update. I think it's super awesome. I think it's a great step in the right direction once they work out the different kinks and bugs and things like that. But I think overall it's a good change and I'm really looking forward to it. So hope you are as well, and I will see you all in the next one.