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

Runes: use $ prefix in variables to denote signals #9401

Closed
olehmisar opened this issue Nov 12, 2023 · 10 comments
Closed

Runes: use $ prefix in variables to denote signals #9401

olehmisar opened this issue Nov 12, 2023 · 10 comments
Labels

Comments

@olehmisar
Copy link

olehmisar commented Nov 12, 2023

Describe the problem

Since runes are to be available only in .svelte.js and .svelte.ts files, so they will be ignored in any other .js or .ts files, why not omit $state and $derived runes entirely and replace them with $ prefix in variable names.

Describe the proposed solution

let name = $state('Alice')
let nameUpper = $derived(name.toUpperCase())

will become

let $name = 'Alice'
let $nameUpper = $name.toUpperCase()

Pros and cons

Pros of $ prefix:

  • let $x = ... is 7 characters shorter than let x = $state(...) and 9 characters shorter than let x = $derived(...)
  • easy to see that a variable is reactive because of the $ prefix

Cons of $ prefix:

  • Developer needs to understand that $ in the variable name makes it reactive. $state and $derived are explicit and may be easier to understand
  • Inconsistency with props. E.g., let { param } = $props(). param is not prefixed with $ but it IS reactive.
  • breaks prop shortening. E.g. <Comp param={$param} />
  • leaking abstraction when using classes:
    class User { $name = 'Alice' }
    will have $name instead of name field. This may be a con but can also be a pro, so consumer of the User class can clearly see that the $name is reactive.

Alternatives considered

-

Importance

nice to have

@levibassey
Copy link

What happens when you don't want $nameUpper to react to changes in $name?

@olehmisar
Copy link
Author

What happens when you don't want $nameUpper to react to changes in $name?

don't add $ prefix: let nameUpper = $name.toUpperCase().

Or a more explicit version: let nameUpper = untrack(() => $name.toUpperCase(). With untrack it's possible to react only to a subset of variables. E.g.,

let $name = 'Alice'
let $lastName = 'Black'
let $fullName = `${$name} ${untrack(() => $lastName)}`
console.log($fullName) // => 'Alice Black'

$lastName = 'Brown'
console.log($fullName) // => 'Alice Black'

$name = 'Bob'
console.log($fullName) // => 'Bob Brown'

@7nik
Copy link

7nik commented Nov 12, 2023

The $derived should be const to provide the ability to control what exactly you want to create and produce the correct read-only type.

let $x = ... is 7 characters shorter than let x = $state(...) and 9 characters shorter than let x = $derived(...)

Also, you can remove whitespace and new lines to shorten your code even more. Less code does not always make the code more readable.

will have $name instead of name field. This may be a con but can also be a pro, so consumer of the User class can clearly see that the $name is reactive.

It still will be possible to create such fields/methods/functions that, in fact, are reactive, or it depends on whether a passed and internally used value is reactive.

So, the extended pros and cons are:

Pros

  • the $ prefix means the variable is reactive (a rune or store, and on the markup, it doesn't matter which one);
  • shifts the focus of the code understanding from incorrect "runes create a reactive value" to the correct "runes create a reactive variable";
  • easy switching variable's reactivity - simply rename it.

Cons

  • lost the ability to pass a second parameter to the runes - maybe even the team cannot tell how important this loss is. Though, a solution can be an explicit definition let $var = $state(0, params);;
  • it limits the class and object shape, or you are forced to use getters and setters to "rid of" the $ in the property name (note, Svelte 5 encourages using classes);
  • breaks prop shortening: e.g. <Comp param={$param} /> and return { prop: $prop };.

Another big problem it creates is misaligning with the $props rune - it also creates reactive variables but their names don't start with $.

@Gin-Quin
Copy link

Gin-Quin commented Nov 16, 2023

I thought a lot about this feature. I was against it at first, but I think it's actually a good idea.

There are actually two ways to do it:

  1. Prefixing all reactive variables with $ like suggested,
  2. Using $ as a prefix operator to get the reactive value of a reactive variable.

Example 1:

<script>
  let $counter = $state(0)
</script>

{$counter}

Example 2:

<script>
  let counter = $state(0) // here we don't prefix with '$'. Instead, we define `counter` as a reactive variable.
</script>

 <!-- Now, we use the $ prefix operator to indicate we want to retrieve the reactive value -->
<!-- That would simply transpile to `$.get(counter)` -->
{$counter}

To me, solution 2 is the cleanest, as you are working with the true values. There is only 1% magic, which is the $ prefix operator for some sugar syntax.

The con of the syntax 2 is that it demands some work to make it work with Typescript compiler. It's not impossible, though, as it has already been done with the old store syntax.

Adopting such a solution would have two great advantages:

1. Readability

It has the advantage of increasing readability. You know if a variable is reactive or not, just by looking at it. Not the most important, but still nice.

2. Solving the issue of these annoying getters

I think the getters are the biggest issue in runes right now. Everyone agrees that's the syntax is very boilerplatey, which is the opposite of what Svelte stands for.

When you create an external reactive variable, like this:

export const counter = $state(0)
<script>
  import { counter } from "./counter.js"
</script>

{counter}

This does not work because the Svelte compiler does not know that counter is a reactive variable. How could it know? You have to get to the type of the variable to be sure that we are handling a reactive variable.

However, with a $ notation, the compiler knows what is reactive or not. If we use our 2nd syntax, we can just do this and it works:

<script>
  import { counter } from "./counter.js"
</script>

{$counter}

It's somehow similar to the old store syntax and the Vue 3 syntax.

What is awesome is that the syntax works very well with signal's fine-grained reactivity (which would not work with stores):

// we have a plain object that has a reactive value
const counter = {
    value: $state(0)
}
<script>
  import { counter } from "./counter.js"
</script>

{counter.$value}
<!-- That would transpile to `$.get(counter.value)` -->

Cons

About the forementioned cons:

lost the ability to pass a second parameter to the runes - maybe even the team cannot tell how important this loss is. Though, a solution can be an explicit definition let $var = $state(0, params);

If you keep the $state call, this is not an issue.

it limits the class and object shape, or you are forced to use getters and setters to "rid of" the $ in the property name (note, Svelte 5 encourages using classes);

It's not an issue with syntax 2.

breaks prop shortening: e.g. and return { prop: $prop };.

It's an issue that should definitely be solved by adding the following shortening:

<Comp {$param} /> shoud be the same as <Comp param={$param} /> (and prefixing property names with $ should be forbidden).

As for { prop: $prop }, I'm not shocked. prop is a reactive variable, and $prop is its inner value. It has the benefit of being very clear.

What do you think?

@7nik
Copy link

7nik commented Nov 16, 2023

  1. Prefixing all reactive variables with $ like suggested,

Well, you are free to name variables as you want.

  1. Using $ as a prefix operator to get the reactive value of a reactive variable.

It won't work correctly in .svelte.ts files - no way to preprocess *.ts files in VS Code, so it will always complain about undefined $variable. And any other ts-tool must somehow understand this magic or work only with compiled files that can be inconvenient, tricky or buggy.

@Gin-Quin
Copy link

Gin-Quin commented Nov 16, 2023

Of course I'm free to prefix my variables as I want, but the idea is to get rid of the "getters problem". Which is, to me, the single biggest issue of runes right now.

It won't work correctly in .svelte.ts files - no way to preprocess *.ts files in VS Code.

Indeed, if there is no way to do so, it would prevent stuff like that:

export const counter = $state(0)
export const doubledCounter = $derived($counter * 2)

Two solutions:

  1. The TS language server protocol allows preprocessing plugins,
  2. You create new extension names like.jsv instead of .svelte.js, and .tsv instead of .svelte.ts

@olehmisar
Copy link
Author

Indeed, if there is no way to do so, it would prevent stuff like that:

export const counter = $state(0)
export const doubledCounter = $derived($counter * 2)

No, it wouldn't. Definition of $state is as follows, which is a totally valid typescript(and javascript) syntax.

declare function $state<T>(initial: T): T;
declare function $state<T>(): T | undefined;

It's just that the svelte compiler treats $state as a special syntax, while typescript typechecker(e.g., vscode) treats it like a function (which does not really exist)

@Gin-Quin
Copy link

I was not talking about $state but about the two syntaxes counter and $counter.

We are defining a variable counter but using $counter next.

This is an issue that happens with syntax #2 only. I you had prefixed your reactive variable with a $, there would have been no issue.

@olehmisar
Copy link
Author

I updated list of pros and cons in the top comment. I am pretty sure that all cons heavily outweigh the pros. But I still wish there was a more concise way to denote a reactive state and derived state. It's too verbose and I don't like that it's inside parenthesis.

let x = ...
$: x = ...

is so much simpler than

let x = $state(...)
let x = $derived(...)

@olehmisar olehmisar closed this as not planned Won't fix, can't repro, duplicate, stale Nov 24, 2023
@Gin-Quin
Copy link

Gin-Quin commented Dec 8, 2023

By the way, the "getters problem" is not a problem anymore with the new proxy system :)

I agree to close this, it was interesting to talk about this subject!

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

No branches or pull requests

5 participants