question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Store is not being updated synchronously / store value is incorrect

See original GitHub issue

Describe the bug Store value is not up-to-date if using a $ subscription inside a subscription function.

To Reproduce

  1. Open this REPL.
  2. Open your browser console.
  3. Click on “Increment” a few times.
  4. Click on “Cause reset”.
  5. Check your console.

For a real life example look at this REPL.

image

Expected behavior Value should be in sync.

Severity Blocking

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:3
  • Comments:17 (6 by maintainers)

github_iconTop GitHub Comments

5reactions
tanhauhaucommented, Jun 22, 2021

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 $store is:

let $store;
store.subscribe(store, (value) => $store = value);

$store is another variable that stays in-sync with the store value by subscribing to it

in most cases, the subscribe callback function is called synchronously, so you can do:

store.set(5);
console.log($store); // 5

so what happened in the above code is that:

  1. store.set(5) will synchronously loop through the subscribers function and call them
  2. (value) => $store = value) is evaluated, therefore the value of $store updated to 5
  3. after all the subscribers are looped through, the store.set(...) method returns, and …
  4. console.log($store) prints out 5

allow me to break down what happen instead, if you write $store = 5, as some of you are confused and wonder what’s the difference between store.set(...) and $store = ...

when you write $store = 5 in a .svelte component, it is compiled into:

store.set($store = 5);
  1. $store = 5 updates the variable $store to 5
  2. the expression $store = 5 returns 5, therefore store.set(5)
  3. store.set(5) will synchronously loop through the subscribers function and call them
  4. (value) => $store = value) is evaluated, therefore the value of $store updated to 5, though at this point, it is already 5
  5. after all the subscribers are looped through, the store.set(...) method returns

strictly speaking, in this case, the value of $store gets update, even before value of the store gets 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-first approach, vs depth-first approach, as explained in https://github.com/sveltejs/svelte/pull/3219#issuecomment-515444121

how would that impact in our case? let’s take a look at the following example:

let store = writable(5);
store.subscribe(() => {
	store.set(10)
	console.log($store);
});
store.set(30);

repl

we subscribe the store and call store.set() in the subscriber function to update the store value to 10, (it wont lead to infinite loop, because internally, writable will not notify the subscribers if the value is set to the same value), so you set it to 30, it will set it to 10 and then it will try to set it to 10 again, which will be a noop.

guess what is the value of $store inside store.subscribe(...) ? you’ll see the function being called a few times, but you’ll find 30 is printed among them!

so, if the store is updated in depth-first approach, whenever you call store.set(...) it will call the subscriber callback function immediately, and update the value of the $store:

  1. store.set(30)
    1. calls (value) => $store = value to update the $store to 30
    2. calls () => { ... }
    3. which calls store.set(10)
      1. calls (value) => $store = value to update the $store to 10
      2. calls () => { ... }
      3. which calls store.set(10)
        1. at this point of time, the value of the store is already 10, so it is a noop.
      4. console.log($store) prints out 10
    4. console.log($store) prints out 10

Run the above code in v3.6.8 which was before the optimisation got introduced, and you’ll see the above REPL

However, now it is run in breadth-first approach:

  1. store.set(30)
    1. calls (value) => $store = value to update the $store to 30
    2. calls () => { ... }
    3. which calls store.set(10)
      1. schedules (value) => $store = value
      2. schedules () => { ... }
    4. console.log($store) prints out 30
    5. calls the scheduled (value) => $store = value to update the $store to 10
    6. calls the scheduled () => {...}
      1. which calls store.set(10)
        1. at this point of time, the value of the store is already 10, so it is a noop.
      2. console.log($store) prints out 10

now 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:

random.subscribe(v => reset())

const updateRandom = () => random.set(Date.now())

function reset () {
	count.set(0)
	console.log('count should be zero ', $count)
}

calling random.set(), which in the subscribe function calls count.set(0) -> causes the (value) => $count = value get scheduled, therefore printing the value out immediately still show the old $count value.

but if you do:

const updateRandom = () => {
  random.set(Date.now())
  console.log('count is zero', $count);
};

you’ll see 0, since random.set() started the store update chain, when it finishes, the value of $random and $count should already been updated.

so, what then?


Workaround / Solution

Use reactive declaration:

$: $random, reset();
3reactions
jwlarocquecommented, Jul 22, 2020

@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(). Calling set() 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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Redux store doesn't get updated synchronously?
Yes, the state updates are synchronous by default unless you are using a middleware like thunk . What's happening here is that, ...
Read more >
Pragma statements supported by SQLite
This pragma works like a query to return one row for each database attached to the current database connection. The second column is...
Read more >
Update-StoreMailboxState (ExchangePowerShell)
The Update-StoreMailboxState cmdlet forces the mailbox store state in the Exchange store to be synchronized with Active Directory.
Read more >
Synchronization issues - Google Workspace Learning Center
Google Workspace Sync for Microsoft OutlookHere's how to troubleshoot synchronization issues ... Getting sync errors or synchronization stopped altogether.
Read more >
How to use promises - Learn web development | MDN
You will have to reload the page each time you enter a new ... .io/learning-area/javascript/apis/fetching-data/can-store/not-found: 404 ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found