Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make export const a way to define constant unreactive props #5572

Closed
pushkine opened this issue Oct 23, 2020 · 21 comments
Closed

Make export const a way to define constant unreactive props #5572

pushkine opened this issue Oct 23, 2020 · 21 comments

Comments

@pushkine
Copy link
Contributor

pushkine commented Oct 23, 2020

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

@rsdavis
Copy link

rsdavis commented Nov 13, 2020

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>

@WHenderson
Copy link

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.

@intelcentre
Copy link

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.

@akiselev
Copy link

akiselev commented May 6, 2021

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.

@aradalvand
Copy link

aradalvand commented Oct 30, 2021

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

@brandonmcconnell
Copy link
Contributor

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

@icecream17
Copy link

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>

@WHenderson
Copy link

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
Copy link

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.

@Oreilles
Copy link

Oreilles commented Dec 1, 2022

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.

@yuiidev
Copy link

yuiidev commented Sep 16, 2023

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.

@dummdidumm
Copy link
Member

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 dummdidumm closed this as not planned Won't fix, can't repro, duplicate, stale Feb 22, 2024
@brandonmcconnell
Copy link
Contributor

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

@dummdidumm
Copy link
Member

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

@brandonmcconnell
Copy link
Contributor

brandonmcconnell commented Feb 22, 2024

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

@dummdidumm
Copy link
Member

dummdidumm commented Feb 24, 2024

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

@Oreilles
Copy link

Oreilles commented Feb 24, 2024

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.

@brandonmcconnell
Copy link
Contributor

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.

@dummdidumm
Copy link
Member

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.

@Oreilles
Copy link

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.

@inta
Copy link

inta commented Nov 4, 2024

Maybe I'm stupid and don't get it, but how do you think a const property is associated with a JS concept?

I see svelte components as functions or constructors, none of which can have const arguments in JS. Const in JS means that the assignment is constant, you can't reassign anything to it. I would understand it to mean that you can't pass the property to the component from the outside (which would make such a public property pointless in the first place). When you pass a constant (with a primitive value like the string in your example) to a function in JS, the constant is passed as a value, not a reference, and if you change it later, the function body will never know. This is the same as the workaround dummdidumm posted.

I stumbled across this issue when I was researching whether it would make sense to destructure props as const (svelte 5). I would really like to understand what specific use case you see for public constant properties.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests