Hi,
Is there any way to disable a new reaction to being triggered due to changes to store.amount in the code below?
const { observable, reaction } = mobx
const store = observable({
price: 1,
amount: 2
})
reaction(
() => [store.price, store.amount],
(data) => {
console.log("REACTION", data)
setTimeout(() => {
store.amount = 5
}, 1000)
}
)
store.price = 4
If I execute that, I get in console
REACTION [4, 2]
REACTION [4, 5]
but if I remove the setTimeout and execute synchronously I get only
REACTION [4, 2]
I want to still run asynchronously with setTimeout but have the changes in the amount inside the async method not to trigger a new reaction as it was running in the synchronously case. Any way to do that ?
Thank you!
Do you need to react only to a change of price and use amount only for some calculation or do you want to also react for a change of amount?
Could you share more info about what the reaction should actually do? Maybe it can be expressed better with computed/s...
Sorry, maybe the names of the variables were not the best - price and amount are independent and I cannot calculate one from the other - I need to react to change of both of them (so I don't think a computed is relevant here)
The above was to give a simple example of async call, my real use case is that I need to call a REST API (promise-based API) and then when it resolves to update the values received (in the above case update amount if reaction was a result of price changed or update price if reaction was a result of amount change). Hopefully, it is clear enough, let me know if not.
Perhaps having localAmount and remoteAmount would work better?
const store = observable({
remoteAmount: null,
localAmount: 2,
price: 1,
amount: computed(function() {
return this.remoteAmount != null ? this.remoteAmount : this.localAmount
})
})
reaction(
() => [store.price, store.localAmount],
(data) => {
console.log("REACTION", data)
setTimeout(() => {
store.remoteAmount = 5
}, 1000)
}
)
store.localPrice = 4;
Also when the field X changes, make sure it changes localX and sets remoteX to null
update amount if reaction was a result of price changed or update price if reaction was a result of amount change
set price(price) {
this._price = price;
// Other value is used for client-server synchronization (so we don't apply obsolete updates), you could use timestamp or something
this.getAmount(this._price, this._amount).then(action((amount, recieved) => { // recieved are the values the server recieved
if (recieved.amount === this.amount && recieved.price === this.price) {
this._amount = amount;
}
}))
}
set amount(amount) {
this._amount = amount;
// Other value is used for client-server synchronization (so we don't apply obsolete updates), you could use timestamp or something
this.getPrice(this._amount, this._price).then(action((price, recieved) => { // recieved are the values the server recieved
if (recieved.amount === this.amount && recieved.price === this.price) {
this._price = price;
}
}))
}
@computed
get price() {
return this._price;
}
@computed
get amount() {
return this._amount;
}
EDIT: If you would really want to prevent the reaction, you could probably dispose the reaction, perform the changes and re-create reaction inside that async callback:
setTimeout(() => {
// dispose
disposer();
// perform changes
store.amount = 5;
// re-subscribe
disposer = createReaction();
}, 1000)
Thank you for your help, but I think the solutions above are bit complex and will be very hard to maintain from a code perspective taking into account I have multiple variables being updated, not only two. The only solution I found feasible is disposing and recreating the reaction inside the async running function, but I still need to think if that doesn't break the functionality if one reaction is triggered after the other.
@mweststrate I'm curious on what are your insights on that? It seems to me that this scenario is a fairly straightforward one and I was assuming it was supported out of the box, the idea is to have a transaction that works async, that means the reaction is not triggered again if I change an observable inside an async call in the sideEffects function.
@alanrubin what we usually do is store a little flag on the process that is going on. I think this pattern is nice because you explicitly describe your exceptions to the normal flow, instead of trying to make mobx itself behave differently in certain cases. If I understood the question correctly, I think you need something like this:
const { observable, reaction } = mobx
let isReceivingData = false
const store = observable({
price: 1,
amount: 2
})
reaction(
() => [store.price, store.amount],
(data) => {
if (!receivingData) {
submitData(data).then(() => {
// every change should propagate as usual, e.g in the UI
// but we want to remember that we are receiving data here
// and as a result don want to submit it back to the server, despite the fact that the data
// might actually change
receivingData = true
store.amount = 5
receivingData = false
})
}
}
)
store.price = 4
Here is an example I came up with: https://jsfiddle.net/7gp845ps/
When the amount gets changed while this.local is set to price, the autorun will not re-run. This is because autorun will only react to changes in the current "local" field.
@mweststrate I like your solution a lot but is it going to work when I use a reaction with a delay? I need that way because I don't want to save immediately data to the backend when it changes but would like to wait a bit to see if I can have an amount of changes saved in one request.
I guess that would not work because once the sideEffect function is triggered after the delay then receivingData is already false. From the top my head I think the solution would be to use a regular reaction (without delay) and do the debounce by myself after checking receivingData. That way I guarantee that the reaction is executed immediately (and receivingData is already true) but still debounce the saving for later. What do you think?
@spion thanks for the nice example, I think I understand your point which is saving exactly what have changed and then checking it in the reaction and acting accordingly. For me, the implementation seems a bit complex if you think I have at least 6 properties that can change, I want to have a delay and be able to update multiple properties in one call as described above (I will probably need to have an array to track instead of a string).
@mweststrate I was testing your solution around "receivingData" flag but for some reason when I run the changes inside a runInAction function (using strict mode) that doesn't seem to work. I cannot really understand what is the difference between running it with or without it.
For example, the solution works well at https://jsfiddle.net/arubin/objxvbom/ (without strict mode). And the same solution doesn't work at https://jsfiddle.net/arubin/objxvbom/4/ (running side effect inside runInAction, reaction never ends in that case). Any ideas why?
Updating the outcome, as I couldn't make it work running inside runInAction I ended up calling the sideEffect function programmatically inside the action for each property change, then I debounce the call, and when executing it afterwards I compare what have changed and act accordingly. Not the most beautiful piece of code (and I'm not using reactions anymore) but it works as expected.
In making some autocomplete searchbox stuff, I think I was in a really similar situation
To solve it, I used Symbols to uniquely identify each "loading operation"
class Store {
@observable input: string = ""
@observable private operation: Symbol = null
@observable loading: boolean = false
@observable suggestions: string[] = []
constructor() {
reaction(() => this.input, input => this.autocomplete(input))
}
private async autocomplete(input: string) {
const operation = Symbol("autocomplete-operation")
this.operation = operation
this.loading = true
const suggestions = await this.getSuggestions(input)
if (this.operation === operation) {
this.suggestions = suggestions
this.loading = false
}
}
private async getSuggestions(input: string) {
// go fetch some suggestions from some api
}
}
Most helpful comment
EDIT: If you would really want to prevent the reaction, you could probably dispose the reaction, perform the changes and re-create reaction inside that async callback: