Variable declarations in markup (replacing {@const ...}
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
feat:
, fix:
, chore:
, or docs:
.packages/svelte/src
, add a changeset (npx changeset
).pnpm test
and lint the project with pnpm lint
Right now we have {@const ...}
, which is a) weird, b) inconsistent with $derived(...)
, and c) limiting.
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).
nice to have
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 await
s 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:
await
inside 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