As a full-time Svelte user for three years, I've built a real-time collaborative website builder with 10 000+ users. My solo success wouldn't have been possible without Svelte, and here's my take on the latest sneak peek of Svelte 5.
First and foremost, a big shoutout to the Svelte team and Rich Harris for their relentless dedication. I've been in love with Svelte for over three years, watching its evolution from the sidelines and loving every update. I consider myself one of its biggest fans. I do not wish to bring any negativitiy, I’m simply showing my perspective as a long term user.
There are 3 parts
How I use Svelte day-to-day today
My takes on Svelte runes
The solution
Part 1:
How I use Svelte day-to-day to build super fast, and the philosophy I've adopted along the way
1. Super lean stores
All my stores are super lean. I keep seeing examples of custom stores like counter:
function createCount() {
const { subscribe, set, update } = writable(0);
return {
subscribe,
increment: () => update((n) => n + 1),
decrement: () => update((n) => n - 1),
reset: () => set(0)
};
}
But this has never been the way I’ve ended up using stores. If I created a counter store I would just use this:
export const count = writable(0);
Typically, I only resort to creating a custom store when I need it to auto-save state. For instance, I might design a custom store that automatically saves its value in localStorage:
export const count = writableLocalStorage('count', 0);
2. Global stores
In my project, I have a file called global.ts
that I really like. This file contains many global stores, making it easy for me to access and change state from anywhere in my codebase with just a quick import. Plus, it's simple to see where each store is being used by doing a quick search in VSCode. Finding and fixing bugs has been trivial. All of the global stores are a simple top-level export like the count example above:
export const count = writable(0);
$state from Svelte 5 cannot be used at the top-level like Svelte stores can, but you can make an writable equivalent out of $state as shown by Rich:
https://twitter.com/Rich_Harris/status/1704889098169709019
export function writable(initial){
let value = $state(initial);
return {
get value () { return value },
set value (v) { value = v },
}
}
3. Related things should be close to each other
There is nothing more satisfying than when a new feature in its entirety neatly fits inside a single .svelte file. My Svelte files often contains a lot of logic and I love it.
Why is this so satisfying? Well because the feature becomes:
Easy to find and fix bugs
Easy to re-learn its context in the future
Easy to find the related logic (If I find the BuyButton.svelte then I will also find the logic that is associated with BuyButton.svelte)
Easy to forget (I don’t need its context in my head when working on other code)
Easy to delete
This is why I love the controversal <script context=”module”> because it allows me to do hacky things like this:
WelcomeModal.svelte
<script context="module">
export const openWelcomeModal = writable(false);
</script>
<script>
/** Logic **/
</script>
<Modal bind:open={$openWelcomeModal}>
<div>stuff</div>
</Modal>
Now it just needs to be mounted somewhere and then it is callable from anywhere in the codebase with a simple
import {openWelcomeModal} from "WelcomeModal.svelte";
$openWelcomeModal = true;
Part 2:
Okay, here are my takes on Svelte 5:
1: You need a mental model of when code runs
I sometimes get bugs from $:{}
where the solution is to either add or remove reactivity dependencies
Debugging starts by having a clear mental modal of when the code is executed, and this has been trivial as the code inside $:{}
is usually short so it is very easy to list all of the reactivity dependencies that will make it run
When I see code like this I get a bit worried:
<script>
let count = $state(0);
function double() {return count * 2}
let double = $derived(double());
</script>
Now reactive dependencies can be hidden in functions. What happens if there are reactive dependencies hidden 5 levels deep inside one of these functions? It will quickly become hard to get a clear mental model of when the code will run
I would really like an $effect(([a,b,c]) => {})
where I could explicity write the reactive dependencies when needed. Pretty much all my ${}
have less than 5 reactive dependencies
2: Runes feel complex
This is sort of a nothing-take, as it is just a general feeling. The original Svelte syntax, with its stores, reactivity and how I could use it to build stuff took me less than a day to understand. It has been 2 days of Svelte runes now and I still don’t really get it.
3: Props
Originally I hated Typescript for one silly reason. I hated that in React I had to essentially write the prop name twice, one for the variable name and another for the type. In Svelte 3 I only had to type the prop name once, but in Svelte 5 i might have to type it twice again.
Somewhere I saw a suggestion like this:
let myProp = $props<boolean>(true);
which is better, but still I don’t like how complex it looks
4: Getters and setters
I really don’t like the verbosity of getters and setters with $state, luckily I’ve read on the discord that there will likely come some kind of syntax sugar that will solve this.
export function writable(initial){
let value = $state(initial);
return {
get value () { return value }, //verbose and confusing
set value (v) { value = v }, //verbose and confusing
}
}
Hopefully there will also be some kind of way to set dynamic names, as that is something I need for some custom stores I’d be making, like:get [dynamic_name]() {return value}
My biggest take
.svelte and .js/.ts shouldn’t be interchangeable
Svelte 5 has a significant goal: ensuring that the JavaScript within the script tag aligns perfectly with the JavaScript in .js/.ts files. However, I respectfully disagree with this approach for one specific reason:
You would lose the ability to do magic
I believe that the reactivity ($state) should be on as default for top-level variables in .svelte files and opt-in for .js/.ts files.
This argument is presented in the Svelte 5 blog post.
“At first glance, this might seem like a step back — perhaps even un-Svelte-like. Isn't it better if let count
is reactive by default?”
“Well, no. The reality is that as applications grow in complexity, figuring out which values are reactive and which aren't can get tricky.”
My response: My codebase is dummy huge, I’ve never had this issue.
“And the heuristic only works for let
declarations at the top level of a component, which can cause confusion.”
My response: I’ve never been confused about this (atleast after I’ve learned it)
“Having code behave one way inside .svelte
files and another inside .js
can make it hard to refactor code, for example if you need to turn something into a store so that you can use it in multiple places.”
My response: Yes, I do happen to refactor some code into stores. This process usually takes just a few seconds and has never posed a significant challenge for me. It's worth mentioning that when I do engage in refactoring, I tend to create new .svelte files rather than creating .js/.ts files. The majority of my code resides within .svelte files.
Interchangeability, for and against:
For:
Makes it easy to copy javascript from .svelte into .js/.ts when refactoring
It is conceptually very satisfying that javascript is identical in .svelte and .js/.ts
Against:
Increases verbosity: (e.g. let count = 0 becomes let count = $state(0))
Harder to read due to verbosity. This might seem like a tiny change, but to me this breaks the game… Everything feels 10x more difficult to read in .svelte files just because of this small little change with $state()
Mixing of reactive and non-reactive variables in .svelte files. I believe that mixing these will cause a lot of confusion in .svelte files. Never in my 3+ years of svelte programming have I ever had the desire or need to have a non-reactive variable within a .svelte file (in the top level)
It is widely recognized that .svelte and .js/.ts files are not identical. As a result, users today do not need to be taught this distinction.
The times I write a variable far exceeds the time I copy something from a .svelte file into a .js/.ts file. The verbosity is just not worth it to save a little bit time when i do refactoring.
Not all footguns are equal. One of the most significant footguns that exists is the useEffect function in React, as it can lead to hard-to-find and hard-to-fix bugs. On the other hand, copying code from a .svelte file into a .js/.ts file can be considered an insignificant footgun because any issues become apparent quickly, as the code will not function in the same way. Fortunately, fixing this issue is relatively straightforward by adding $state() to the variables. Once this lesson is learned, it is unlikely to be forgotten, preventing similar experiences in the future.
I also just want to say that it is more important to do easy things easier than it is to do hard things easier. Why? Because 95% of the time you will be doing easy things and 5% of the time you will be doing hard things, atleast that has been my experience for the past 3 years.
Part 3:
Introducing Pelte,
the successor to Svelte 5
I’m considering making a Svelte-preprocessor for Svelte 5 that takes a more svelte-like syntax and outputs the Svelte 5 rune syntax. Maybe a fun name would be pelte? react → preact, svelte → pelte? haha
Pelte in action:
<script lang="context">
let x = 0; //non-reactive
export function aFunction(){};
</script>
<script type="pelte">
export let myProp: boolean = true;
let a = 5;
let b = 7;
$: c = a * b;
$:{console.log(c)}
function func(){
let d = 25;
let f = $state(2);
$:y = d * f;
}
</script>
compiles to
<script>
let x = 0;
export function aFunction(){}; //Can you even export stuff in Svelte 5?
const {myProp = true} = $props<{myProp:boolean}>();
let a = $state(5);
let b = $state(7);
const c = $derived(a * b);
$effect(() =>{console.log(c)});
function func(){
//variables within functions does not become reactive, only top-level variables do
let d = 25;
let f = $state(2);
let y = $derived(d * f);
}
</script>
All Svelte 5 needed was a compiler before it compiles lmao
Maybe there could be a VSCode plugin that allows you to toggle between Svelte 5 and Pelte within a file, to make it easy to copy reactive code out of the .svelte file?
Summary of its rules:
Make all top-level variables in the script tag into
$state()
Get all export let props and make it into the Svelte 5 syntax for props
Make
$:c = a * b
intolet c = $derived(a * b);
Make
$:{console.log(c)}
into$effect(() => {console.log(c)})
Copy code from context=”module” into the script as is
Variables within functions act the same way as in .js/.ts files, so you have to opt-in to reactivity
Final words
let count = 0;
vs let count = $state(0);
is a very very big deal for me.
The magic of Svelte isn't ‘let count = 0’, it's ‘count += 1’
Just sounds like cope to me. They’re both important, and way more important than interchangeability between .svelte and .js/.ts files
Follow me
That’s it, thanks for reading! Follow me on twitter if you want to:
I have totally the same opinion. Thanks for the article.
I'd take a preprocessor that only does "Make all top-level variables in the script tag into $state()". I'm down with the rest of the changes, but this is the thing still bugging me.