Skip to content

Svelte 5: Add property change hook #10236

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

Open
brunnerh opened this issue Jan 19, 2024 · 8 comments
Open

Svelte 5: Add property change hook #10236

brunnerh opened this issue Jan 19, 2024 · 8 comments

Comments

@brunnerh
Copy link
Member

brunnerh commented Jan 19, 2024

Describe the problem

It was already a pain to enforce consistent internal component state in v4, it's now even harder with v5 because $effect will cause infinite loop issues.

$derived is not going to solve this, it serves a different function entirely.

Describe the proposed solution

Add a hook that runs on external changes to the props, the callback inside will not be an effect so any changes to or normalization of internal state or props should not cause the handler to re-run. E.g.

import { onPropChange } from 'svelte';

let { values, disabled, maxLength } = $props();

onPropChange(e => {
  if (disabled)
    values = [];

  values = values.slice(0, maxLength); // would loop in $effect
});

The event object could be supplied with the name of the property that was changed, and maybe even old and new values.

Importance

would make my life easier

@dummdidumm
Copy link
Member

Could you give more information why you need this / examples where this would be useful?

@brunnerh
Copy link
Member Author

One example was given in #9944, where people tried to reproduce the required behavior with both $effect and $derived which resulted either in convoluted/buggy $effect code or the wrong behavior because $derived cannot change state like props. I noted in a comment how easy this would be, using the proposed hook.

I have usually run into this when properties have dependencies amongst themselves, often related to validation or coercing ranges via a minimum/maximum.

Or instead of a range, a certain format has to be ensured, so if a value is set on the component from the outside, the UI has to be updated, but on user interaction that should not be the case, as it would interfere with typing. Even if you allow the interference, if you try to do this via reactivity, having an underlying and a formatted value, you run into circular dependencies or, in the case of Svelte 5, infinite effect loops. There are some ways around this, but they are unintuitive and complicated.

@dm-de
Copy link

dm-de commented Jan 20, 2024

In v5, today, here a missing link between bind:prop in parent component and same prop in child component.
why?

  1. child prop (which is bound in parent) can not have a default value
  2. you can not use $derived to change bounded prop value
  3. it seems possible to use $effect for this - but it is hard (specially with arrays!!!) and can easily cause problems with endless updates.

edit:
I would like to explicitly point out the problems with arrays - perhaps there is a good solution specifically for arrays?

And after using $effect, I fully understand that $effect is not a replacement for $:
It is different.

I tried to write about my experience with $effect, but it was closed. #9944
But believe me - this problem will keep you busy with v5, v6, v7 etc. until a solution is found.

onPropChange() is something like afterUpdate - right?
But will trigger only, after some prop changed - right?
It should not trigger, if YOU change bounded prop inside child component

Solutions from other frameworks:

  1. Vue is able to prevent loops - I tested this:
    Svelte 5: $effect is unusable (produce circular dependencies and endless updates) #9944 (comment)

  2. It seems that Preact have value.peek() to prevent this manually... Is this like untrack? I'm not sure...
    https://github.com/preactjs/signals#signalpeek

@Antonio-Bennett
Copy link

Hey maybe I’m a bit confused here but when bounding props from parent to child isn’t your default value going to be defined in the parent since that’s the flow of the variable? Parent -> child. If you want a default value in child assign that in parent since it’s being passed in?

@dm-de
Copy link

dm-de commented Jan 20, 2024

bind:value is a two-way binding
parent and children can change it

@mjadobson
Copy link

For my own interest, I had a go at implementing the example use case with $effect():

<script>
  let { values = $bindable(), disabled = $bindable(), maxLength = $bindable() } = $props();
  
  $effect(() => {
    if (!values.length) return;
    if (disabled) values = [];
    if (values.length > maxLength) values = values.slice(0, maxLength);
  });
</script>

End result is ok, but debugging infinite loops isn't a great dev experience 😅. untrack isn't very useful here because you want to listen to all prop changes; the trick is to use conditionals to stop assignment (and thus the loop) once the desired result has been met.

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAE41STY-bMBD9K7PWSgsVItmueiFAteqpVbc9dG8hBwJDYtXYlj1EG1H-e2VIMKlaqQdLnq83z--5Zw0XaFmy7ZksW2QJe9aaRYzO2gX2hIKQRcyqzlQuk9rKcE15IQvirVaG4JNqtZIoCRqjWniIV3MmngAeNoV0AwIJTqXo0EIG95ZKwmD7GL2Pnnbh5trQlm9fUR7o6Hs--GrNbbkXWPtiUwqLriFdeW4y3XdESoKSleDVz6wPQsjyy_ZYd_YYvJR0jBuhlAke1-_GyJSyVm0QhuGQP9c1fCZs09UElRfyF_wDdsHq7nof8ld1OAicizdAL-UbTM9MIOVSdwRO86xgsmv3aAoGey7rZCSc9bMoA6ym9x3N9eb19xN2unstx3CmucpZxFpV84ZjzRIyHQ7R_AVmwP_9CM6YfuGsW-YWBWF0Y9gyf2OzL8DgYm2UtsHkujv32DRYUTCp3Y9p4g0EdxdDxQgVgkHqjNz4huv60NPb7hb1m3nIPa3FwKXHCl5hsF5QnwgOf3y-tOYnsHQWmChdVpzOWT-r8BHW8RMk8DiMyvVffnz_FlsyXB54c77QCQcHWPPTX2zaDb8BZLc2TLUDAAA=

@gloriousjob
Copy link

gloriousjob commented Nov 5, 2024

@dummdidumm
Not sure where I'd ask this but I think this ticket kind of provides the idea I would think would be needed:
Example scenario is a color carousel that can take a color as a bindable and list of supported colors and has a state to keep track of where in the carousel it is (let's call this location) and some objects filled with a color. When an object is selected, it updates the carousel with the supported colors and the color causes the carousel to update the location state to where the color is present and then to highlight the color. Clicking a next or previous button will update the location but without the highlighted color (unless the user clicks a color after).

The problem I'm having is that the effect is resetting the location to the selected color when you click next so it ends up not showing the next list of options. Perhaps I'm thinking about the state wrong and I'd love to know how to think properly about it if so.

@gloriousjob
Copy link

gloriousjob commented Nov 7, 2024

@dummdidumm Not sure where I'd ask this but I think this ticket kind of provides the idea I would think would be needed: Example scenario is a color carousel that can take a color as a bindable and list of supported colors and has a state to keep track of where in the carousel it is (let's call this location) and some objects filled with a color. When an object is selected, it updates the carousel with the supported colors and the color causes the carousel to update the location state to where the color is present and then to highlight the color. Clicking a next or previous button will update the location but without the highlighted color (unless the user clicks a color after).

The problem I'm having is that the effect is resetting the location to the selected color when you click next so it ends up not showing the next list of options. Perhaps I'm thinking about the state wrong and I'd love to know how to think properly about it if so.

Sorry, I'm going to rescind the need for this request as I figured out that I had more state variables in my component than needed and the old structure allowed for it. After refactoring the variables, I found the problem went away. I think the way to describe it was that I was linking state. I appreciate the consideration though!

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

No branches or pull requests

6 participants