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

add a way to force update a state variable #14520

Open
olivierchatry opened this issue Dec 3, 2024 · 29 comments · May be fixed by #14639
Open

add a way to force update a state variable #14520

olivierchatry opened this issue Dec 3, 2024 · 29 comments · May be fixed by #14639

Comments

@olivierchatry
Copy link

olivierchatry commented Dec 3, 2024

Describe the problem

I'm using a custom made data store with realtime update. In svelte 4, when the value got updated, I was just doing value = value to refresh different bindings. In svelte 5 it is not possible anymore as before updating bindings, the system check if the value is the same. What more is, inside some of the object, we have our classes that are filled automatically ( think relationships ) and so it seems like svelte is not detecting the change in values.

I'm not sure if I'm clear in what I'm explaining, but basically, I would need something to inform the system to update bindings forcefully.

To be a bit more precise:

The system we have in my company needs to be used without svelte, so we have our own data system. We have so call "Resource" and "ResourceCollection" ( that are basically relationships ). The system handle realtime update accross client using web sockets. To get "informed" of these change we subscribe to the resource. The only way I found to make the refresh working is by doing that ( subscribeAndDo is internal to our system ) :

  let unsub
  $effect(() => {
    unsub?.unsubscribe?.()
    unsub = account.subscribeAndDo(onDestroy, () => {
      const newAccount = account
      account = null
      account = newAccount
    })
  })

Describe the proposed solution

The goal here is, whenever the props "account" change, we subscribe to it. Then whever something change in "account" the callback will be called. Here, I'm using the "trick" to make sure that svelte update the bindings. It would be great if we can have a $forceUpdate(account) instead. if that make any sense.

Importance

would make my life easier

@dummdidumm
Copy link
Member

Why does account need to be force-updated exactly? Are there some things in the template that use it that should rerun? If yes, in which way should they rerun?

It sounds like you want a solution like this: #10560 (comment)

@dummdidumm dummdidumm added the awaiting submitter needs a reproduction, or clarification label Dec 3, 2024
@olivierchatry
Copy link
Author

Yes, there is some thing in the template that use it. Looking at the solution you provided, I do not want to wrap things on top of things when a simple force update function would do the job ( seems a lot cleaner and a lot less code ).

@olivierchatry
Copy link
Author

As far as I can tell my issue if svelte 4 vs svelte 5 is there :

https://github.com/olivierchatry/svelte/blob/2e57612ef495689e628078e988126f057cc2f7a8/packages/svelte/src/internal/client/reactivity/sources.js#L158-L160

export function internal_set(source, value) {
	if (!source.equals(value)) {

@dummdidumm dummdidumm added feature request and removed awaiting submitter needs a reproduction, or clarification labels Dec 3, 2024
@webJose
Copy link
Contributor

webJose commented Dec 3, 2024

I think OP wants something like $state.signal() that marks the reactive value as changed, which should trigger all effects associated to the signaled state, but without having the value actually changed (skipping the check for equality).

@olivierchatry
Copy link
Author

olivierchatry commented Dec 3, 2024 via email

@Thiagolino8
Copy link

Wouldn't it be better if $state.raw behaved like the state declared with let did in V4?

This type of issue appears quite frequently around here

One of the biggest advantages of svelte over other frameworks is precisely the fact that it doesn't need its own ecosystem and works well with vanilla libraries

And one of the factors that contributed the most to this was precisely the fact that it was possible to have reactivity in external objects simply by reassigning them

I agree that it wasn't a good default behavior, but as an optional behavior it was more useful than $state.raw

@olivierchatry
Copy link
Author

I agree with you, as I have a lot of issues porting my ( quite big ) application. But still, I would rather have a signal function so that I can control myself when to actually trigger reactivity, as some state are actual props, so have a state.raw will mean you need something to declare props as raw as well.

@Leonidaz
Copy link

Leonidaz commented Dec 3, 2024

It's likely that currently there doesn't exist a straightforward solution but is there a way you could provide a minimal reproduction just client side only if possible? Just so there is total clarity.

client side: Playground
or full-stack sveltekit: Sveltelap

@david-plugge
Copy link

I feel like If this gets added it would confuse people. To me it sounds like you would be better of writing a wrapping class for your state and using a setter to explicitly run code instead of relying in sveltes reactivity system which is meant to reduce execution time cost by identifying identical values and skipping a rerun.

@olivierchatry
Copy link
Author

olivierchatry commented Dec 3, 2024 via email

@Leonidaz
Copy link

Leonidaz commented Dec 3, 2024

I'm thinking that borrowing the idea of an equality function option from Angular's signals might be a possible solution for such cases.

https://angular.dev/guide/signals#signal-equality-functions

It would only work for $state.raw(), and for $state() at the top level, since it can be a deeply nested proxy (basically only if it's reassigned)

Upon set, instead of using Svelte's pre-defined equality function, equals(), the provided equality function is used. In this particular case it can just always return false.

But for other cases, upon the top level reassignment, the deeply nested properties can be compared and a decision can be made if a change occurred.

It would work with the current $state.raw() or $state(), even with an empty initial state, e.g. $state.raw(undefined, { equal: (prev, value) => false }). This second parameter would be a pojo, future proofing for any additional options. The usage will not break any of the existing Svelte 5 code as the second parameter is optional.

This new option for testing equality would also be helpful for comparing NaN's where in some cases, you'd want NaN's to be equal to each other, like in Object.is()

@olivierchatry
Copy link
Author

olivierchatry commented Dec 3, 2024 via email

@Leonidaz
Copy link

Leonidaz commented Dec 3, 2024

I understand, but I still think that having a function to force trigger reactivity is a must when working with vanilla JS libraries, which was in 4 a real strength of svelte ( and one of the reasons we decided to switch to it ).

I'm not sure I understand.

Having a custom equality option would do what you want upon reassignment. Same as the force option that you were adding to the source code. But it would allow more flexible options.

E.g.

let account = $state.raw(prop, { equal: () => false });

....

// to make reactive, reassign as this will call `set` and then `internal_set` which will call the custom `equality`.

account = account;

@olivierchatry
Copy link
Author

olivierchatry commented Dec 3, 2024 via email

@trueadm
Copy link
Contributor

trueadm commented Dec 4, 2024

We're not going to change how $state or $state.raw works so these kind of hacks can work; and we most definitely won't be introducing a new rune just for these cases. The only possible thing that we could maybe consider is a custom equality function – as mentioned above.

@olivierchatry
Copy link
Author

Great, what the arguments against it ? It's not a hack to use a library that is not using svelte data store to manage it's data I think ?

@olivierchatry
Copy link
Author

olivierchatry commented Dec 4, 2024

It's likely that currently there doesn't exist a straightforward solution but is there a way you could provide a minimal reproduction just client side only if possible? Just so there is total clarity.

client side: Playground or full-stack sveltekit: Sveltelap

Here it is, it's really minimal but does kind of show how it works now :

https://svelte.dev/playground/97ab67f0c4fa405c9f11eee13ba574b0?version=5.5.3

Note, is is normal that I have to do :

        let theAccount = globalGraph.accounts['1']
	let account = $state(theAccount)
	
	theAccount.notify = function (cause, delta) {
		account = null
		account = theAccount
	}

instead of directly :

	let account = $state(globalGraph.accounts['1'])	
	account.notify = function (cause, delta) {
                const theAccount  = account
		account = null
		account = theAccount
	}

Isn't proxy suppose to proxy the object directly ? or am I missing something. It's so weird that to make things "less magic" it make them actually more obfuscated.

@trueadm
Copy link
Contributor

trueadm commented Dec 4, 2024

Isn't proxy suppose to proxy the object directly ? or am I missing something. It's so weird that to make things "less magic" it make them actually more obfuscated.

The object you pass to $state is essentially a new copy of the object, so mutating the original is not the right approach here.

@olivierchatry
Copy link
Author

Well, that is a big bummer. What would be the right approach for you when you have want to use a separate data library ? I guess wrapping everything into a big class that is declared as state and uses the Resource object from the said library - but that is adding a deep layer of glue code for no good reason I think ?

@Leonidaz
Copy link

Leonidaz commented Dec 4, 2024

@olivierchatry are any of these a possibility?

<script>
	import  { globalGraph } from './GlobalGraph.js'
	
	let name = 'world';
	let theAccount = globalGraph.accounts['1'];
	let account = $state({});
	
	theAccount.notify = function (cause, delta) {
		Object.assign(account, delta);
	}
</script>

<h1>Hello {account.counter}!</h1>

Playground

or this:

<script>
	import  { globalGraph } from './GlobalGraph.js'
	
	let name = 'world';
	let theAccount = globalGraph.accounts['1'];
	let account = $state.raw({});
	
	theAccount.notify = function (cause, source) {
		account = source;
	}
</script>

<h1>Hello {account.counter}!</h1>

Playground

@Leonidaz
Copy link

Leonidaz commented Dec 4, 2024

@olivierchatry

There is also the following possibility:

<script>
	import  { globalGraph } from './GlobalGraph.js'
	
	let name = 'world';
	let account = $state(globalGraph.accounts['1']);
	globalGraph.accounts['1'] = account;
</script>

<h1>Hello {account.counter}!</h1>

Playground

@olivierchatry
Copy link
Author

Yes for this example, as it is quite simple, but for reference, this is a video of our application https://youtu.be/-0myn3FsuEs?si=vwv2eDHxu1iGuRsH there is a lot of code involved and a lot of vanilla JavaScript interacting with our svelte components.

If we have to redo /rewrap everything, I fear
it looks more and more as rewriting all the software as we made assumption regarding how svelte was handling data and never really thought about the next version changing everything.

So it would be nice if at least we could trigger reactivity manually. It's actually not a lot of code ( as far as I can tell, I did the code but did not find out how to test my branch yet ). So I'm wondering why not make a small change ?

@webJose
Copy link
Contributor

webJose commented Dec 4, 2024

My personal opinion is that the request for the new functionality is reasonable. Being able to trigger effects for an unchanged value is something most won't need, and that's OK because it is "extra" functionality for the minority of people that may have a rightful need to do this.

I am no expert in JS libraries, but it is conceivable that there are libraries out there that, for example, produces objects that are instances of classes, and we know Svelte won't make those reactive. A potential rune $state.signal() sounds like a winner to me to cover cases like this one, especially because the needed work in the framework appears to be small.

As time goes by, I bet people will find more usefulness for such feature.

@abdel-17
Copy link

abdel-17 commented Dec 4, 2024

Can’t you do this in user land with a simple utility?

class Signal {
current = $state.raw();

constructor(current) {
this.current = current;
}
}

something = new Signal(…)

Something.current = … (this is reactive now)

@olivierchatry
Copy link
Author

olivierchatry commented Dec 4, 2024 via email

@kwangure
Copy link
Contributor

kwangure commented Dec 5, 2024

If you find yourself refactoring because you're unable to "force-update a variable". You might find createSubscriber useful.

See the recently shipped https://svelte.dev/docs/svelte/svelte-reactivity#createSubscriber.

@olivierchatry
Copy link
Author

It is nice, but it means that I need to wrap my classes around that, and change all my code to use an attribute in the wrapper class so that it svelte knows it's reactive. This will be a lot of structural changes.

Also I will add that this add a bit more of "magic" to the mix on my view, as transforming an attribute ( current in the sample ) as reactive becase you have call a subscriber on it in a separate class is really "weird". I'm guessing there is probably a nice state machine to handle these inside svelte ? whereas calling $state.signal is, I think, a bit more "self explanatory" in what it does.

@trueadm trueadm linked a pull request Dec 9, 2024 that will close this issue
@sourcecaster
Copy link

sourcecaster commented Dec 25, 2024

Here's another example (which is derived from my project in production):

<script>
	class A { /* in fact it's declared somewhere else which 
		     prevents using $state inside the class declaration */
		param = 15;
		update() {
			this.param += 10;
		}
	}

	let item = $state(new A());
	
	function handleClick() {
		item.update(); /* implicit change happened and I know it 
		                  happened, so I need to trigger reactivity manually */
		// item = item  - that's what I used to do in Svelte 4 and it worked. 
		// In Svelte 5 I'm forced to do this instead:
		let tmp = item;
		item = null;
		item = tmp;
	}
</script>

<div onclick={handleClick}>{item.param}</div>

I'm dealing with a lot of class instances in my real project. All of them have inner state which is often changed on various events or actions. I use reassigning very often to make it work. Need a better way for sure!

@izznat
Copy link

izznat commented Dec 30, 2024

Meanwhile, you can use a helper like below that uses createSubscriber. It's just a function. The helper is in createSignal.js file. I edited @olivierchatry example to use the helper: Playground

The downside is, you need to get and set the value via signal.value like in Vue to trigger the getter and setter.

<!-- App.svelte -->
<script>
	import { createSignal } from './createSignal.js'
	import { globalGraph } from './GlobalGraph.js'

	// Usage 1
	let account = createSignal(globalGraph.accounts['1'])
	account.value.notify = function (cause, delta) {
		account.value = account.value
	}
	
	// Usage 2
	// let account = createSignal((signal) => {
	// 	let account = globalGraph.accounts['1']
	// 	account.notify = function (cause, delta) {
	// 		signal.value = account
	// 	}
	// 	return account
	// })

</script>

<svelte:options runes />
<h1>Hello {account.value.counter}!</h1>
// createSignal.js
import { createSubscriber } from 'svelte/reactivity'

export function createSignal(init) {
	let value
	let update
	
	let subscribe = createSubscriber((update_function) => {
		update = update_function
	})

	let signal = {
		get value() {
			subscribe()
			return value
		},
		set value(new_value) {
			value = new_value
			update()
		}
	}

	if (typeof init === 'function') {
		value = init(signal)
	} else {
		value = init
	}
	
	return signal
}

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

Successfully merging a pull request may close this issue.