Variable declarations in markup (replacing {@const ...}
#16490
{@const ...}Development PRs
Currently, not every part of your component can be asyncβ child fragments can't be. A fragment is the children inside of a block, such as an {#if } block. Most fragments are compiled to synchronous functions. Since template expressions are individually compiled to functions that are either async or sync depending on their values, they can be async inside synchronous fragments. However, other things, such as {@const }, can't be async, since you can't have await expressions inside synchronous functions.
This fixes that. Additionally, this makes certain features, such as variable declarations in markup, much easier to implement.
Closes #16462 Supersedes #16463
Before submitting the PR, please make sure you do the following
- It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- Prefix your PR title with
feat:,fix:,chore:, ordocs:. - This message body should clearly illustrate what problems it solves.
- Ideally, include a test that fails without this PR but passes with it.
- If this PR changes code within
packages/svelte/src, add a changeset (npx changeset).
Tests and linting
- Run the tests with
pnpm testand lint the project withpnpm lint
Allows {let/const ...} declarations in all places (and more) where we already allow {@const ...} (which will eventually get deprecated in favor of this new feature).
Closes: #16490
Companion PRs:
Issue
Describe the problem
Right now we have , which is a) weird, b) inconsistent with $derived(...), and c) limiting.
Describe the proposed solution
Instead of this...
{#each boxes as box}
{@const area = box.width * box.height}
{box.width} * {box.height} = {area}
{/each}
...we could do this:
{#each boxes as box}
{let area = $derived(box.width * box.height)}
{box.width} * {box.height} = {area}
{/each}
Or if you want it to be read-only, use const instead. You get the idea.
This would also solve a limitation that people periodically encounter β the fact that you can't create local state. This is particularly irksome if a snippet needs to do something stateful, since you now need to convert it to a component (including carefully moving over any CSS that it uses):
{#snippet counter()}
{let count = $state(0)}
<button onclick={() => count += 1}>
clicks: {count}
</button>
{snippet}
{@render counter()}
{@render counter()}
{@render counter()}
Naturally, $state.raw and $derived.by would also be supported, as would normal non-state declarations. We could also allow declarations with multiple declarators:
{let a = 1, b = 2, c = 3}
Migrating existing uses of {@const ...} in runes-mode components would be trivial...
{@const x = y}
{const x = $derived(y)}
...and we could deprecate it at the same time.
This is more powerful, looks nicer, is one less bit of weird non-JavaScript syntax, and is something that would have come in handy a number of times recently.
Scoping would work the same as it does in snippets, i.e. bounded by the parent item (whether that's an element, block or whatever). So this would be possible...
<div>
{let a = 1}
{a}
</div>
<div>
{let a = 1}
{a}
</div>
...but this would be an error because the declaration is duplicated in the same scope:
<div>
{let a = 1}
{a}
{let a = 1}
{a}
</div>
Declarations should precede usage. We probably don't want to allow var since it would be confusing to have different scoping rules, and different scoping rules are the only thing that distinguishes var from let.
We probably do want using, at least eventually (see #16192).
Importance
nice to have
Info
The local state problem can be worked around via wrapping, but this looks like a good idea anyway π
{@const count = reactive(0)}
<button onclick={() => count.current += 1}>
export function reactive(initialValue) {
const value = $state({ current: initialValue });
return value;
}Very neat, is the "immediate child" restriction going away?
Probably not, is there a reason you can't declare the variables in your <script> tag?
Probably not, is there a reason you can't declare the variables in your
<script>tag?
That was just out of curiosity around the design, now that the feature is growing from useful to convenient.
At this point I would also vote for getting rid of this limitation.
Declarations generally should be close to where they are being used; artificially enforcing more separation does not seem to make much sense. The new design gives a lot of flexibility, keeping this one rule feels quite arbitrary.
It is also not necessarily about top level declarations, e.g. if you have a lot of content/nesting in an #each:
{#each items as item}
<div class="card">
<header>
...
</header>
{const value = ...}
<p>Value is {value}</p>
</div>
{/each}Yeah, you'd be able to use these anywhere, top-level or otherwise
Much needed! Especially because they will just work anywhere in the markup.
Before this I was writing {#if true} {@const value = 1} ... {/if} if I wanted a local variable scope.
(And before {@const} existed I was writing it as {#each [1] as value} ... {/each} π)
Will the script tag be necessary after this feature is merged?
{
let count = $state(0);
const increment = () => count++;
}
<button onclick={increment}>{count}</button>Yes, since for most components there's more to their code than just variables π Also, small nitpick, you can't do multiple declarations in one pair of curly braces. You can do multiple declarators, which is a fancy term for this sort of thing:
{let foo, bar = 0, baz = null} Ok, sure. Just out of curiosity, what is the reason for restriction on the type of expressions, statements allowed within the { } boundary?
what is the reason
The template is, well... a template β to turn it into a view you Just Add Water Data. Expressions therein are reactive and (unless you go rogue) side-effect-free. It is the output of the whole process of reactivity. Things update as-needed, in a non-linear order.
Statements on the other hand are the elements of a program. A program runs linearly, top to bottom, then exits. What does this mean in the context of a template? Nothing, really. It's incoherent.
Would awaits inside of these variable declarations behave the same as await elsewhere in the template, avoiding waterfalls?
I'm asking as right now remote functions are fairly painful to use inside of the template outside of trivial cases. Imagine a remote function that takes a bunch of arguments and has many properties that need to be displayed. You end up with something like this:
{#if await getTodo({tenantId: tenant.id, projectId: project.id, todoId: id})}
<h1>{(await getTodo({tenantId: tenant.id, projectId: project.id, todoId: id})).title}</h1>
<img src={(await getTodo({tenantId: tenant.id, projectId: project.id, todoId: id})).author.image} />
<span>Author: {(await getTodo({tenantId: tenant.id, projectId: project.id, todoId: id})).author.name}</span>
<p>Created at: {(await getTodo({tenantId: tenant.id, projectId: project.id, todoId: id})).createdAt}</p>
{:else}
Todo not found
{/if}
Right now I'm playing around with creating promises in the script and awaiting in the template but I think {let todo = await ...} has potential to be preferable, at least in some cases.
Not automatically, no β the straightforward implementation of something like this...
{let x = $derived(await one())}
{let y = $derived(await two())}
...would entail creating x then y. (They will update independently after that, but are waterfalled on creation.) #16561 is the issue to watch for that; my hope is that whatever static analysis tricks we come up with to make it possible to update template chunks out of order will also make it possible to de-waterfall things like deriveds
I see. So for someone looking to solve the following combination of problems:
- Avoid waterfalls
- Avoid the verbosity of calling and awaiting the same function each time it's needed in the template
awaitinside of a component's boundary (since code inside of the script is outside)
The solution would be a combination of this issue and #16561?
And in the interim it seems like the next closest solution is create the promise inside of script and await it inside of the template each time it's needed. So we still need to await each time, but can avoid the pain and error-proneness of repeating the function call all over the place.
Thanks
how about something like a {#scope} or {#local}?
{#scope let a = 123}
{/scope}
and anything inside the scope (or under same parent) will be able to access the scope.
A very similar thing was discussed in the {@const ...} RFC and as shown, it can get pretty ugly when you have multiple variables.
Hello @Rich-Harris
Statements on the other hand are the elements of a program. A program runs linearly, top to bottom, then exits. What does this mean in the context of a template? Nothing, really. It's incoherent.
With regards to this lets looks at the simple example in Solid:
function Counter() {
return <div>
{(() => {
const [count, setCount] = createSignal(0);
let x = 42;
console.log("foo", x);
return <button onClick={() => setCount(count => count + x)}>
{count()}
</button>
})()}
</div>;
}
This maps 1 to 1 to the imaginary Svelte syntax.
{#snippet counter()}
<div>
{@
let count = $state(0);
let x = 42;
console.log("foo", x);
}
<button onclick={() => count += x}>
{count}
</button>
</div>
{/snippet}
So what do you mean by "it's incoherent"?
For context , but signals there can't be created in the JS code, they use their own tag-like syntax, that is partly for easier compile-time analysis, there is no really an equivalent of toplevel Svelte <script></script>, basically their constraints are different.
which allow you to insert code pretty much anywhere using dollars $, with the caveat that it had a non-signal based model similar to React.
Please support multiple declarations. Maybe something like:
{#script}
let count = $state(1);
let double = $derived(count*2);
{/script}How is
{#script}
let count = $state(1);
let double = $derived(count*2);
{/script}
better than
{let count = $state(1)}
{let double = $derived(count*2)}
? It's additional syntax, and it's more typing.
Also @kanashimia
{#snippet counter()}
<div>
{let count = $state(0)}
{const x = 42}
{console.log("foo", x)} <!-- This is already a thing you can do -->
<button onclick={() => count += x}>
{count}
</button>
</div>
{/snippet}
You can call functions with void output in the markup. They'll be called each time their dependencies change, which in this example case is only once.
? It's additional syntax, and it's more typing.
Yes, but the syntax is similar to real js syntax except for the wrapping. And actually if you have a lot of variable declarations, a single wrapper is less typing than a pair of curly bracket on every line.
You can do something like
{let foo = 0, bar = 1, baz = 2}That is just disgusting, though.
Same reason we don't do that in the script tag. Even though this is only a stylistic choice, there's an about it.
You can call functions with void output in the markup. They'll be called each time their dependencies change, which in this example case is only once.
@sillvva Yes, that is true! Although I would consider that more like a hack really.
What I had in mind though was more along the lines of untracked context, so console.log(count) won't rerun.
Basically a feature to fully subsume toplevel <script>, exactly like it but nestable.
So you are able to use any runes and write any code as you already can, just without seemingly arbitrary limitations of component syntax. Though this feature seems bigger in scope and as such more complicated to add, just throwing an idea out there.
With regards to syntax Marko 5 had both inline and block syntax:
$ {
let count = 1;
let double = count * 2;
}
$ let count = 1;
$ let double = count * 2;
So it is possible to have both. Maybe svelte compiler could be smarter with detecting those statements so that something like this would be allowed:
{
let count = $state(1);
let double = $derived(count*2);
}
{let count = $state(1)}
{let double = $derived(count*2)}
In my opinion though denoting this with something other than just { } would make more sense, to not conflate with template interpolation.
What about having multiple <script> tags in one component/page? That would be closer to plain HTML, would be easy to parse, would naturally allow multiline code without looking odd, and would not necessitate any new magical syntax in the markup.
{#snippet counter()}
<div>
<script lang="ts">
let count = $state(0);
let x = 42;
console.log("foo", x);
</script>
<button onclick={() => count += x}>
{count}
</button>
</div>
{/snippet}
The one annoying thing with this is having to write lang="ts" everwhere, though that could easily be solved by declaring a default <script> tag language in svelte.config.js, or perhaps any child <script> tags just follow the primary one at the beginning of the page.
@firatciftci That would be a breaking change sadly as Svelte already allows to nest script tags and they are interpreted as regular html script tags.
@kanashimia I don't see why nested script tags wouldn't be able to continue working as regular HTML script tags, with added support for runes and other Svelte-specific functionality. Seems like a non-breaking change to me, though I don't know the internals of Svelte code that well.
Because they are executed when the component is mounted, and svelte doesnβt compile those afaik Not when the component is initialised, which is when the top level script is executed (or at least the compiled version thereof)
Pro tip: You can prefix GitHub URLs of issues, PRs or discussions with svcl.dev/ to view them on this page! Also try it on a GitHub release ;)