svelte
svelte copied to clipboard
Make `export const` a way to define constant unreactive props
It is often the case that component props happen to be either by intent or de facto constant and unreactive
Unfortunately Svelte does not provide a way to define unreactive props, in doing so it outputs a substantial amount of superfluous code, makes components less shareable , and misses an otherwise fantastic opportunity for the compiler to identify "unreactive", so called pure components on its own to optimize their output accordingly
While Svelte features the ability to define props using export const
, props defined by that syntax can only be derived from other props, and cannot be set directly.
Described in the documentation as a way to define "readonly" props, it mistakenly draws parallels for some of us to Typescript's readonly
class property modifier which, contrary to Svelte's const props, defines readonly properties that in fact can be set directly on init
For those reasons, and because it would greatly enhance an otherwise very rarely used feature, I believe that there is a great case to make for const props to be settable on init
This proposed change asserts for the following to render 42
<script>
export const answer = 1.618;
</script>
{answer}
<Component answer={42} />
Most scenarios where this change would qualify as breaking also qualifies as an unintended use case as values passed to const props currently throw unknown prop
dev warnings. With that said this is still a breaking change in cases where export const
is used in combination with $$props
, as the latter suppresses unknown props dev warnings.
I do not expect this to be implemented as it is technically breaking, but considering the value it could hypothetically add to the framework I still think it's worth putting a proposal out there
Possibly related #5183
So, you're suggesting that const
props could be set externally. What happens when answer
changes value?
<Component { answer }/>
<button on:click={() => answer += 1}>Increment</button>
So, you're suggesting that
const
props could be set externally. What happens whenanswer
changes value?<Component { answer }/> <button on:click={() => answer += 1}>Increment</button>
My suggestion would be that the original component is destroyed and a new one is created in its place with the new const value. This would be similar to how the #key directive currently works.
In support of the proposal, i would suggest that the lack of this feature is the cause of some bugs. Where a component author has used an input with the assumption that it would not change, they likely will not have created reactive initialisation code. Then, when the input changes the component becomes only partially updated.
Im on a phone so providing an example is hard right now. Let me know if I am being unclear and I will provide an example when I am at a computer.
Im on a phone so providing an example is hard right now. Let me know if I am being unclear and I will provide an example when I am at a computer.
Here is an example, with a workaround: https://svelte.dev/repl/693e994fa12248efbd8d2700db97727d?version=3.29.7
Ideally we could come up with a way to automatically produce an equivalent of the workaround.
My suggestion would be that the original component is destroyed and a new one is created in its place with the new const value. This would be similar to how the #key directive currently works.
That behavior would be extremely surprising and unintuitive.
I think export const
is better reserved for exposing interfaces on components though bind:this
.
I think this is a great idea. Have had many similar cases myself, this would be really useful.
As a newcomer to Svelte, not having a way to define constant props (with a supported default/fallback value) feels like a risk to the reliability of data passed between components. In situations where props are passed down a few layers, if one of those prop values changes along the way, the app may not operate as expected, and this would not be considered a bug with Svelte.
That said — I think adding support for immutable props would be a huge advantage for all Svelte developers.
export const importedValue = 5;
// │ │ └────┐ │
// prop immutable variable default (fallback) value
I'm also a newcomer to svelte.
One problem when doing this is that I also have to add a default value, so this is invalid syntax:
<script>
// ColorOption.svelte
export const name: StyleOption;
</script>
Here's another workaround that seems less unintuitive than using key
:
<script>
$: {
name
throw ReferenceError("name is immutable")
}
</script>
@pushkine what would be the use case for this? It seems like a workaround for user error - if you can change a let
declaration to const
in order to get the behaviour you want, you can also make any intermediate calculation reactive.
In @intelcentre 's example, the correct workaround is to simply move the intermediate calculation into the expression $: output = JSON.stringify({ roInput, rwInput, intermediateCalculation: roInput * rwInput });
. There is no extra cost to that, and declaring the inputs as const
, only to force a remount when it changes, takes the exact same though process.
Svelte currently supports input/output and output props (see #1
and #2
) , but does not support input only props which is what I believe is being discussed here.
<script>
// 1) input/output prop - supports input (a={...}) and input/output (bind:a={...}) syntax
export let a;
// 2) output props - can be accessed via bind:this={self}/self.b and input/output (bind:b={...}) syntax
export const b = /* ... */ 0;
export function c() { /* ... */ };
// 3) non-reactive code - code may be based on input props, but this code will not be re-run when they are updated
/* ... */
// 4) reactive code - code here will be re-un if any of the referenced properties are updated
$: { /* ... */ }
</script>
I am finding that this missing feature leads to components being defined with syntax that suggests mutable props but whos implementations expect those inputs to remain constant.
Whilst there are exotic cases where it makes sense for non-reactive code to reference the initial values of mutable props, in most situations this is almost always a source for bugs. It would be far better to be able to define these props as immutable. This way, component users would get clear indication of which properties are designed to be reactive and which are not. It may also be helpful for svelte to issue warnings when non-reactive code references mutable props.
syntax for an immutable prop
If immutable input props were to be added, what would the syntax be?
Some suggestions:
- Change the semantics of
export const b = ...;
to allow the b={...} input syntax (this would be a breaking change for a few reasons ☹️). - Add an additional keyword. e.g.
export readonly let value
- would break so many things - Use some sort of prefix to denote const-ness. e.g.
export let const_value
- just yuk - Use some sort of comment. e.g.
export /* svelte:readonly */ let value
- still yuk - Hopefully someone else can suggest something better - 🙏
changing an immutable prop
If immutable input props were to be added, what would happen if a new value was assigned to them?
e.g. <Component immutable_value={changing_value} />
.
Possible solutions:
- Issue a warning when attempting to update an immutable value. (I think this would have to be a runtime concern)
- Reconstruct the component with the new value as if the component were inside a
{#key {...imutable_props}}
block - Offer multiple solutions based on a
<svelte:options />
flag - Hopefully someone else can suggest something better
In @intelcentre 's example, the correct workaround is to simply move the intermediate calculation into the expression
$: output = JSON.stringify({ roInput, rwInput, intermediateCalculation: roInput * rwInput });
. There is no extra cost to that, and declaring the inputs asconst
, only to force a remount when it changes, takes the exact same though process.
(FYI, I was @intelcentre - work account) You are correct that the problem can be solved by making unreactive blocks reactive, but the example was a simple contrivance rather than real world code.
In my experience, writing complex components where all inputs are reactive can easily get quite complex. const
inputs would be a way to cut down that complexity and allow a certain amount of natural compiler help. Just as const
is not technically necessary for javascript to function, it sure is a nice bit of sugar.
Another parallel would be how languages such as C# allow classes to have readonly member variables. If you think of components as class instances, readonly member variables would be a direct parallel to what is being discussed here.
I also find that exported const variable, but still assigned by parents would be a really great addition to Svelte.
The only alternative is currently to add Typescript' readonly
prop on input variable you plan to keep untouched, but it comes with the drawback that every function you'd like to pass those variable too must also declare them as readonly
, which is rarely the case in existing codebases / library - and more importantly that this is only a syntactic declaration, that requires Typescript, but wouldn't actually prevent the forbidden behavior at runtime (since the variable would still be declared mutable).
So, you're suggesting that
const
props could be set externally. What happens whenanswer
changes value?<Component { answer }/> <button on:click={() => answer += 1}>Increment</button>
My suggestion would be that the original component is destroyed and a new one is created in its place with the new const value. This would be similar to how the #key directive currently works.
I think that a sensible implementation would rather fail compilation of the given example, and only allow constants to be passed to exported constants. So given:
// Component.svelte
export const answer
Only this would be possible:
// OK
<Component answer=42 />
// OK
<script> const answer = 42 </script>
<Component { answer }/>
And this would fail:
// Error: assigning mutable variable `answer` to const export
<script> let answer = 42 </script>
<Component { answer }/>
As nice as it would be, it wouldn't even be a breaking change: const export
already weren't mutable wether by the parent or the child component. Children initialized const export
would just have to be overriden by the value passed by the parent, if any.
Also new to Svelte, so forgive me if I'm stating something incorrect.
Allowing a way to define a constant prop could also circumvent the issue when, in my case, creating a single TextInput
component which handles all the input types that essentially render as text e.g. text
, email
, password
, etc. results in an error complaining that type
can't be dynamic when value
is bound to.
TextInput.svelte
<script lang="ts">
export let type: 'date' | 'email' | 'month' | 'text' = 'text'; // etc etc
export let value;
</script>
<input {type} bind:value={value} />
Which could then elegantly be reused similar to vanilla HTML inputs by setting the type property.
I initially figured I could fix the error by making it a const, but of course that means the parent component cannot set the prop on initialization / creation of the component.
With Svelte 5 on the horizon things are shifting more towards the runtime, so the code savings for having this kind of "set once" property are negligible, both bundle-size-wise and performace-wise, therefore closing.
@dummdidumm I've been watching this issue for a while now. I would counter the closure reason, as this issue is critical to DX. Svelte still offers no way to set up nonreactive props. This issue solves that.
Why is that important, other than theoretical bundle size and performance savings?
@dummdidumm For the same reason it's important to have both const
and let
variables. To purposely create props that can be passed in but are intentionally not meant to be change anywhere else in the component.
In JS, we can theoretically use let
everywhere and just try to remember not to change them when they're meant to be constant, but that's what const
is for.
In this case, you would pass in a value for a const
prop and it would continue to serve as a const throughout the component, so it cannot be updated or changed later whether reactively or not.
In the case you need this it's very easy to do yourself:
// Svelte 4
export let fixed;
export let dynamic;
const _fixed = fixed;
// Svelte 5 runes
let { fixed, dynamic } = $props();
const _fixed = fixed; // use _fixed everywhere else
This does not prevent fixed
from being reassigned, neither from the parent, nor from the child. Seems like a recipe for mistakes, both for the component user (who would not understand why updating fixed
has no effect) and for the component developper (who could mistakenly use a variable instead of the other).
Having a way to explicitly declare that a property cannot and shouldn't be updated seems like a reasonable use case, considering that JS itself has const
.
I agree with @Oreilles that this actually feels like a mistake and recipe for disaster in both syntaxes, with the traditional approach and with the new rune-based approach.
This does not prevent fixed from being reassigned, neither from the parent, nor from the child. Seems like a recipe for mistakes, both for the component user (who would not understand why updating fixed has no effect)
This is a documentation problem. The property name and its documentation should suggest that this is static. If there was a separate concept built-in to Svelte, you wouldn't see if the property is static from the other side either.
and for the component developper (who could mistakenly use a variable instead of the other).
When a component developer opts for a static property (which is a very rare case; I still haven't heard a compelling use case) they are likely ok with the additional complexity
Having a way to explicitly declare that a property cannot and shouldn't be updated seems like a reasonable use case, considering that JS itself has const.
const
does not prevent mutation though. There's no real immutability here. I don't think shallow immutability warrants a separate concept to learn and maintain.
This is a documentation problem. The property name and its documentation should suggest that this is static.
Why should we have to add documentation stating that something should not be reassigned, without any guarantee or protection against someone doing it, when JS already has a way to define constants and natively prevent it ? This seems to go against the very spirit of Svelte.
When a component developer opts for a static property (which is a very rare case; I still haven't heard a compelling use case) they are likely ok with the additional complexity
There are many cases where it would make no sense that a property changed during a component lifecycle, and where you'd want that property to be declared as const
. Just for the sake of giving an example, a board game where you define the grid size at initialization.
const
does not prevent mutation though. There's no real immutability here. I don't think shallow immutability warrants a separate concept to learn and maintain.
Shallow immutability is not a new concept since as you state, that's already what const
does. And as I stated in a previous comment, this feature wouldn't require any syntactic, conceptual or breaking changes. Just pure JS all the way down, as intended by Svelte's mantra.