Skip to content

add a way to force update a state variable #14520

@olivierchatry

Description

@olivierchatry

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

Activity

dummdidumm

dummdidumm commented on Dec 3, 2024

@dummdidumm
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)

olivierchatry

olivierchatry commented on Dec 3, 2024

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

olivierchatry commented on Dec 3, 2024

@olivierchatry
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)) {
webJose

webJose commented on Dec 3, 2024

@webJose
Contributor

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

olivierchatry commented on Dec 3, 2024

@olivierchatry
Author
Thiagolino8

Thiagolino8 commented on Dec 3, 2024

@Thiagolino8

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

olivierchatry commented on Dec 3, 2024

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

Leonidaz commented on Dec 3, 2024

@Leonidaz
Contributor

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

david-plugge commented on Dec 3, 2024

@david-plugge

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

olivierchatry commented on Dec 3, 2024

@olivierchatry
Author
Leonidaz

Leonidaz commented on Dec 3, 2024

@Leonidaz
Contributor

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

olivierchatry commented on Dec 3, 2024

@olivierchatry
Author

12 remaining items

olivierchatry

olivierchatry commented on Dec 4, 2024

@olivierchatry
Author
kwangure

kwangure commented on Dec 5, 2024

@kwangure
Contributor

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

olivierchatry commented on Dec 6, 2024

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

sourcecaster

sourcecaster commented on Dec 25, 2024

@sourcecaster

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

izznat commented on Dec 30, 2024

@izznat

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
}
sourcecaster

sourcecaster commented on Feb 15, 2025

@sourcecaster

It would be really nice to have some reactivity trigger syntax like:

let myObject = $state(new ForeignClass());
$state.trigger(myObject);
sourcecaster

sourcecaster commented on Feb 22, 2025

@sourcecaster

After a lot of testing and playing around I've found a promising solution! It turns out that you can use writable to achieve the desired behavior:

<script>
    import { writable } from 'svelte/store';
    class A { /* declared anywhere */
        param = 15;
        update() {
            this.param += 10;
        }
    }

    let item = writable(new A());
	
    function handleClick() {
        $item.update();
        $item = $item;
    }
</script>

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

It also works with components' $bindable parameters!

<script>
    let { item = $bindable() } = $props();
</script>

<input type="text" bind:value={item.param}>
kwangure

kwangure commented on Feb 24, 2025

@kwangure
Contributor

Building on this pattern to reset state when props change, I found a strategy to force manual updates for derived values.

function computed<T, U>(
	value: () => T,
	transform: (input: T) => U,
	start: (update: () => unknown) => void | (() => void),
) {
	let stop: void | (() => void);
	const _derived = $derived.by(() => {
		const _value = value();
		untrack(() => {
			stop?.();
			stop = start(
				() => (_derived.current = untrack(() => transform(_value))),
			);
		});
		let state = $state({ current: untrack(() => transform(_value)) });
        return state;
	});

	return _derived;
}

When you change a non-reactive value, you trigger an update the signal manually.

let grid = $state<NonReactive>(new Grid());
let value = computed(
	() => grid,
	(v) => v.data.x, // derive deep `grid` value
	(update) => {
	    const id = setInterval(() => {
			grid.data.x++;
			update(); // force update (i.e. value.current = grid.data.x)
		}, 1000);
		return () => clearInterval(id);
	}
);
assert(value.current == grid.data.x)

But it works best if the thing triggering manual updates can be contained in the start function. (e.g. WebSocket, scroll listeners or resize/mutation observers)

let node = $state<HTMLElement>(); // set later when DOM is ready
const height = computed(
	() => node,
	(node) => node?.clientHeight ?? 0,
	(update) => {
		if (node) {
			const observer = new ResizeObserver(update);
			observer.observe(node);
			return () => observer.disconnect(); // cleaned up when `node` changes
		}
	},
);

REPL

EDIT:
You could use a height.update() method too for external triggering!

let node = $state<HTMLElement>(); // set later when DOM is ready
const height = computed(
	() => node,
	(n) => n?.clientHeight ?? 0,
);

$effect(() => {
    if (!node) return;
		
	const observer = new ResizeObserver(() => {
		height.update() // force update
    });
	observer.observe(node);
	return () => observer.disconnect(); // cleaned up when `node` changes
})

REPL

linked a pull request that will close this issue on Apr 3, 2025
ayyash

ayyash commented on Jul 23, 2025

@ayyash

My two cents, I'm using the mighty RxJS, and I still sometimes run into situations where the inner component simply refuses to pick up the new state :/
I haven't checked the reason yet, but it's making a crack in my soul

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Participants

      @Leonidaz@ayyash@olivierchatry@trueadm@dummdidumm

      Issue actions

        add a way to force update a state variable · Issue #14520 · sveltejs/svelte