- Start Date: 2020-09-09
- RFC PR: #33
- Svelte Issue: (leave this empty)
Add a new {@const ...}
tag that defines a local constant.
Consider a component with an each
block:
<script>
export let boxes;
</script>
{#each boxes as box}
<div
class="box"
style="width: {box.width}px; height: {box.height}px"
>
{box.width} * {box.height} = {box.width * box.height}
</div>
{/each}
Suppose we'd like to add a large
class for boxes over 10,000 square pixels. We could do it like this...
<script>
export let boxes;
</script>
{#each boxes as box}
<div
class="box"
class:large={box.width * box.height >= 10000}
style="width: {box.width}px; height: {box.height}px"
>
{box.width} * {box.height} = {box.width * box.height}
</div>
{/each}
...but that duplicates the box.width * box.height
expression. If we were to add medium
and jumbo
classes, this would quickly get out of hand. We could define a helper function instead...
<script>
export let boxes;
const area = box => box.width * box.height;
</script>
{#each boxes as box}
<div
class="box"
class:large={area(box) >= 10000}
style="width: {box.width}px; height: {box.height}px"
>
{box.width} * {box.height} = {area(box)}
</div>
{/each}
...but adding logic to the <script>
is unfortunate, and we're still calculating the area twice per box.
We could create what our forebears called a 'viewmodel'...
<script>
export let boxes;
const boxes_with_area = boxes.map(box => ({
...box,
area: box.width * box.height
}));
</script>
{#each boxes_with_area as box}
<div
class="box"
class:large={box.area >= 10000}
style="width: {box.width}px; height: {box.height}px"
>
{box.width} * {box.height} = {box.area}
</div>
{/each}
...but that feels like a hack.
These situations crop up from time to time, and the various workarounds are labour-intensive, involve wasted computation, and impede readability and refactoring.
Suppose we added a new {@const ...}
tag to the Svelte template language. We could define area
where it is used:
<script>
export let boxes;
</script>
{#each boxes as box}
{@const area = box.width * box.height}
<div
class="box"
class:large={area >= 10000}
style="width: {box.width}px; height: {box.height}px"
>
{box.width} * {box.height} = {area}
</div>
{/each}
The @const
indicates that the value is read-only (i.e. it cannot be assigned to or mutated in an expression such as an event handler), and communicates, through its similarity to const
in JavaScript, that it only applies to the current scope (i.e. the current block or element).
Attempting to read a constant outside its scope would be a reference error (unless the constant was already shadowing a value, of course):
{#each boxes as box}
{@const area = box.width * box.height}
<!-- ... -->
{/each}
<p>this is a reference error: {area}</p>
With JavaScript const
(and let
), values cannot be read before they have been initialised. We could choose to do the same thing here, though in our case it is an unnecessary restriction — we could very simply hoist values to the top of their block, in the order in which they're declared:
{#if n}
<p>{n}^4 = {hypercubed}</p>
{@const squared = n * n}
{@const cubed = squared * n}
{@const hypercubed = cubed * n}
{/if}
🐃 You could argue that this improves or degrades readability depending on your perspective. You could also argue in favour of topological ordering, as we have in the case of reactive statements/declarations. I don't yet have strong opinions one way or the other.
Defining the same constant twice in a single block would be a compile error:
{@const foo = a}
{@const foo = b}
I've provisionally suggested {@const ...}
for the reasons articulated above, namely the similarities to const
, so this new feature could be called a 'const tag'. The keyword is open to bikeshedding though — perhaps someone could make a case for calling it {@value ...}
or {@local ...}
instead.
Accepting this proposal wouldn't require any changes to the existing docs, but would require some new documentation to be created.
The evergreen answer to the 'why should be not do this?' question is 'it increases the amount of stuff to learn, and is another thing that we have to implement and maintain'.
In this case though there's an additional reason we might consider not adding this. One of the arguments that's frequently deployed in favour of JSX-based frameworks over template-based ones is that JSX allows you to use existing language constructs:
{boxes.map(box => {
const area = box.width * box.height;
return <div
class="box"
class:large={area >= 10000}
style="width: {box.width}px; height: {box.height}px"
>
{box.width} * {box.height} = {area}
</div>
})}
The complaint is that by choosing less powerful languages, template-based frameworks are then forced to reintroduce uncanny-valley versions of those constructs in order to add back in missing functionality, thereby increasing the mount of stuff people have to learn.
In general, I'm unpersuaded by these arguments (learning curve is determined not just by unfamiliar syntax, but by unfamiliar semantics and APIs as well, and the frameworks in question excel at adding complexity in those areas). But this is a case where it feels like we're papering over a deficiency in our language, and is the sort of thing detractors might well point to and say 'ha! see?'. Whether or not that's a reason not to pursue this RFC is a matter for collective judgment.
The alternatives are shown above in the 'motivation' section; they are not good.
- Is
@const
the best keyword? - Should we allow multiple declarations per tag? (Probably not.)
- Does TDZ apply?
- Are declarations ordered as-authored, or topologically?