sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
78.42k stars 4.11k forks source link

Svelte 5: Add property change hook #10236

Open brunnerh opened 8 months ago

brunnerh commented 8 months ago

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 commented 8 months ago

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

brunnerh commented 8 months ago

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 commented 8 months ago

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. https://github.com/sveltejs/svelte/issues/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: https://github.com/sveltejs/svelte/issues/9944#issuecomment-1863423090

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 commented 8 months ago

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 commented 8 months ago

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

mjadobson commented 4 months ago

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=