svelte: Store is not being updated synchronously / store value is incorrect
Describe the bug
Store value is not up-to-date if using a $ subscription inside a subscription function.
To Reproduce
- Open this REPL.
- Open your browser console.
- Click on “Increment” a few times.
- Click on “Cause reset”.
- Check your console.
For a real life example look at this REPL.

Expected behavior Value should be in sync.
Severity Blocking
About this issue
- Original URL
- State: open
- Created 4 years ago
- Reactions: 3
- Comments: 19 (6 by maintainers)
First of all,
store.set(value)is synchronous.if you call
store.set(5), you should be able to read the value out of the store immediately,get(store) === 5.Secondly, the magic of
$storeis:$storeis another variable that stays in-sync with the store value by subscribing to itin most cases, the subscribe callback function is called synchronously, so you can do:
so what happened in the above code is that:
store.set(5)will synchronously loop through the subscribers function and call them(value) => $store = value)is evaluated, therefore the value of$storeupdated to5store.set(...)method returns, and …console.log($store)prints out5allow me to break down what happen instead, if you write
$store = 5, as some of you are confused and wonder what’s the difference betweenstore.set(...)and$store = ...when you write
$store = 5in a.sveltecomponent, it is compiled into:$store = 5updates the variable$storeto 5$store = 5returns5, thereforestore.set(5)store.set(5)will synchronously loop through the subscribers function and call them(value) => $store = value)is evaluated, therefore the value of$storeupdated to5, though at this point, it is already5store.set(...)method returnsstrictly speaking, in this case, the value of
$storegets update, even before value of thestoregets update, but it all happen within the same statement, it is unlikely to have a race condition.Now, if we are all aligned with the fundamentals of
store, here is what introduced this bug:https://github.com/sveltejs/svelte/pull/3219
which introduced an optimisation to update store value via
breadth-firstapproach, vsdepth-firstapproach, as explained in https://github.com/sveltejs/svelte/pull/3219#issuecomment-515444121how would that impact in our case? let’s take a look at the following example:
repl
we subscribe the store and call
store.set()in the subscriber function to update the store value to10, (it wont lead to infinite loop, because internally,writablewill not notify the subscribers if the value is set to the same value), so you set it to30, it will set it to10and then it will try to set it to10again, which will be a noop.guess what is the value of
$storeinsidestore.subscribe(...)? you’ll see the function being called a few times, but you’ll find30is printed among them!so, if the store is updated in
depth-firstapproach, whenever you callstore.set(...)it will call the subscriber callback function immediately, and update the value of the$store:store.set(30)(value) => $store = valueto update the$storeto30() => { ... }store.set(10)(value) => $store = valueto update the$storeto10() => { ... }store.set(10)10, so it is a noop.console.log($store)prints out10console.log($store)prints out10Run the above code in
v3.6.8which was before the optimisation got introduced, and you’ll see the above REPLHowever, now it is run in
breadth-firstapproach:store.set(30)(value) => $store = valueto update the$storeto30() => { ... }store.set(10)(value) => $store = value() => { ... }console.log($store)prints out30(value) => $store = valueto update the$storeto10() => {...}store.set(10)10, so it is a noop.console.log($store)prints out10now see that the inner
store.set(10)schedules the update of the variable$store, therefore you see$store=30?that’s exactly what happened in this issue:
calling
random.set(), which in the subscribe function callscount.set(0)-> causes the(value) => $count = valueget scheduled, therefore printing the value out immediately still show the old$countvalue.but if you do:
you’ll see 0, since
random.set()started the store update chain, when it finishes, the value of$randomand$countshould already been updated.so, what then?
Workaround / Solution
Use reactive declaration:
@jhwheeler as pushkine said, when you use assignment Svelte assumes that assignment is what you want and sets the store value immediately, regardless of what then happens in
set(). Callingset()directly avoids this behavior (might be desirable for custom stores), though frankly I think you’d usually be better off naming the method something else and avoiding the confusion of it also being called after assignment.I was confused enough by this to ask about it on Stackoverflow. I agree with antony; I would have expected
set()and assignment to have the same behavior by default.AFAIK in the docs it says
store.set(value)is the proper way to set a store’s value, so I would say it’s a bug that it doesn’t work as described.And if
$count = 0works, butcount.set(0)doesn’t, then why do we even have the function syntax? 😕 We should just use the assignment syntax everywhere instead.I am running into this and having to unwind a ton of code to try and exactly identify where the problem(s) originates. I typically used $store over store.set but had to use store.set when using in my typescript portions. Was the optimization gain from https://github.com/sveltejs/svelte/pull/3219 worth the side-effect?
In fact, in my real-world example the second store is a store.
I need the
filterto be a store because it is part of the app state. So, iffilteris a store anyway and I need to perform an operation when its value changes, how is it a “bad use” to perform this operation in the subscribe function? 🤔Anyway, the point of this thread is to report an obvious bug and not to discuss code quality.
svelte store sets are always synchronous
in your examples, the second store is not a store you’re only using the event dispatching feature of the store
the issue is that stores updated by other stores are assumed to be derived stores, so svelte treats them differently
$count = 0does work because it’s known “for sure” that$countwill be set to0, so it sets the value locally at the same time as it callscount.setthat however is willy-nilly compiler optimization that should be removed, as that causes stores such as
tweenedandspringto be temporally out of sync with the local component value and it only works locally as$countwill still be out of sync in other componentsthe value should be set synchronously anyway so setting it locally at
set_store_valuebefore it gets set incomponent_subscribeis just unnecessary extra work that only hides bad uses of stores such as this oneIt does work, but it is async.
I think
$count = 0;has something like a “auto await tick”, because if you change theresetfunction like thisit works.