svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Make `export const` a way to define constant unreactive props

Open pushkine opened this issue 4 years ago • 11 comments

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

pushkine avatar Oct 23 '20 11:10 pushkine

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>

rsdavis avatar Nov 13 '20 21:11 rsdavis

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>

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.

WHenderson avatar Nov 14 '20 06:11 WHenderson

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.

intelcentre avatar Nov 17 '20 02:11 intelcentre

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.

akiselev avatar May 06 '21 19:05 akiselev

I think this is a great idea. Have had many similar cases myself, this would be really useful.

aradalvand avatar Oct 30 '21 10:10 aradalvand

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

brandonmcconnell avatar Mar 01 '22 22:03 brandonmcconnell

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>

icecream17 avatar Mar 03 '22 03:03 icecream17

@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.

ricardobeat avatar Apr 11 '22 13:04 ricardobeat

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:

  1. Change the semantics of export const b = ...; to allow the b={...} input syntax (this would be a breaking change for a few reasons ☹️).
  2. Add an additional keyword. e.g. export readonly let value - would break so many things
  3. Use some sort of prefix to denote const-ness. e.g. export let const_value - just yuk
  4. Use some sort of comment. e.g. export /* svelte:readonly */ let value - still yuk
  5. 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:

  1. Issue a warning when attempting to update an immutable value. (I think this would have to be a runtime concern)
  2. Reconstruct the component with the new value as if the component were inside a {#key {...imutable_props}} block
  3. Offer multiple solutions based on a <svelte:options /> flag
  4. Hopefully someone else can suggest something better

WHenderson avatar Apr 15 '22 14:04 WHenderson

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.

(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.

WHenderson avatar Apr 15 '22 14:04 WHenderson

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 when answer 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.

Oreilles avatar Dec 01 '22 01:12 Oreilles

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.

yuiidev avatar Sep 16 '23 22:09 yuiidev

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 avatar Feb 22 '24 17:02 dummdidumm

@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.

brandonmcconnell avatar Feb 22 '24 18:02 brandonmcconnell

Why is that important, other than theoretical bundle size and performance savings?

dummdidumm avatar Feb 22 '24 19:02 dummdidumm

@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.

brandonmcconnell avatar Feb 22 '24 19:02 brandonmcconnell

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

dummdidumm avatar Feb 24 '24 11:02 dummdidumm

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.

Oreilles avatar Feb 24 '24 14:02 Oreilles

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.

brandonmcconnell avatar Feb 24 '24 21:02 brandonmcconnell

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.

dummdidumm avatar Feb 26 '24 09:02 dummdidumm

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.

Oreilles avatar Feb 26 '24 12:02 Oreilles