Hi, everyone.
I found https://github.com/mobxjs/mobx-state-tree/blob/master/docs/async-actions.md, but can't understand how to apply it to my case:
const Something = types
.model({
value: 0,
// some other stuff
})
.actions(self => ({
afterCreate() {
setInterval(() => { // How to do this?
self.value = self.value + 1;
}, 1000);
},
// some other actions
}));
I could expose callback from setInterval as an action to make it works, but it's actually internal behaviour and shouldn't be accessible from outside.
What's the right way to do such stuff?
I don't think you can have private actions really. As long as it needs to modify model properties, it needs to be returned from the actions to have it properly handled.
export const Store = types
.model('Test', {
value: 0,
})
.actions(self => ({
afterCreate() {
setInterval(self.increaseValue, 1000)
},
increaseValue(inc = 1) {
self.value += inc
},
}))
When you think about it, modifying a model public state shouldn't be considered the "internal behavior". You want to be able to track changes to your state.
Sidenote: I do not recommend doing side effects like starting timers in afterCreate as it makes the code less predictable and harder to test. Imo it's a better practice to expose eg. start and stop actions to have full control over it. Or even consider moving timer logic out of store completely. Models are not meant to serve as a _controller_ also ;)
Models are not meant to serve as a controller also ;)
If model is not supposed to be used as a "controller" then loading data inside model shouldn't be proposed as well, imho. But mobx-state-tree even provides flow helper for such stuff.
That's not the point :) You can have actions that run data fetching, but it shouldn't be done automatically when a model is created. By a _controller_ I mean exactly that, to control how the application behaves. Invoking an action from the outside is perfectly fine.
ok, let's use explicit actions to enable/disable this behaviour:
const Something = types
.model({
value: 0,
// some other stuff
})
.actions(self => {
let intervalId = null;
return {
start() {
intervalId = setInterval(() => {
self.value = self.value + 1;
}, 1000);
},
stop() {
clearInterval(intervalId);
intervalId = null;
}
};
}));
What's wrong here?
Well, you are still missing action for an actual state change. Check the example in my previous post.
Don't get me wrong, but I don't see a big semantic difference between my example and example for async actions from doc:
.actions(self => ({
fetchProjects: flow(function* fetchProjects() { // <- note the star, this a generator function!
self.githubProjects = []
self.state = "pending"
try {
// ... yield can be used in async/await style
self.githubProjects = yield fetchGithubProjectsSomehow()
self.state = "done"
} catch (e) {
// ... including try/catch error handling
console.error("Failed to fetch projects", error)
self.state = "error"
}
})
}))
Example from doc doesn't have action for "an actual state change" as well, it has fetchProjects action which modifies state many times.
Also, it uses flow which your example is not :) It's a whole different story with that. Also, timers are not async per nature, you would have to wrap that into a promise returning function (or use some promisify module) to be able to yield it.
I recommend reading this thoroughly for a better understanding ... https://mobx.js.org/best/actions.html
Especially first paragraph is important here.
The action wrapper / decorator only affects the currently running function, not functions that are scheduled (but not invoked) by the current function! This means that if you have a setTimeout, promise.then or async construction, and in that callback some more state is changed, those callbacks should be wrapped in action as well!
You are still missing the key point of my question focusing on insignificant details. I've read documentation, I understand why the example from doc works but mine doesn't. I got that mobx-state-tree doesn't have direct solution or helper like flow to implement such stuff and I need to find some workaround to make it work.
Anyway, thank you for your response.
In that case, I don't understand your point at all. Please provide a relevant example to show an issue at hand. If you are using timers, I've shown you what you need to do. The basic mechanic is, that you either wrap every state modification to an action (it has to be a simple function without callbacks) or you use flow to have it wrapped automatically for you.
Your original question was: _What's the right way to do such stuff?_
I have shown you one way. There is never a right way in programming, everything has tradeoffs and its own ugliness. You have to pick what you like the most.
Hi folks,
I've been solving a similar problem, specifically related to a socketio connection that has to be conditionally started, and should be closed once the missing/expected data was available; it also should allow to be closed on demand (user action, route changes, etc). I was able to solve it without increasing the model's API surface, that's it, without adding actions meant to be private.
This problem applies to any kind of subscription interface, like observables (e.g., RxJS).
The "solution" involves converting the subscription, which pushes data/events, into a generator which you can pull data from; this generator has to return promises, which complies to what flow expects.
import { types, flow, onSnapshot } from 'mobx-state-tree'
const defer = () => {
const bag = {}
return Object.assign(
new Promise((resolve, reject) => Object.assign(bag, { resolve, reject })),
bag
)
}
const intervalGenerator = function * intervalGenerator (ms) {
let nextValue = defer()
const intervalId = setInterval(() => {
nextValue.resolve()
nextValue = defer()
}, ms)
try {
while (true) {
yield nextValue
}
} finally {
clearInterval(intervalId)
}
}
const Something = types
.model({
counter: 0
})
.actions((self) => {
let intervalIterable
return {
start: flow(function * start () {
intervalIterable = intervalGenerator(1000)
while (true) {
const { done, value } = intervalIterable.next()
if (done) return
self.counter += 1
yield value
}
}),
stop () {
if (intervalIterable) intervalIterable.return()
intervalIterable = null
}
}
})
const store = Something.create()
// listen to the changes
onSnapshot(store, (snapshot) => {
console.dir(snapshot)
})
// Start the counter
store.start()
// Stop after some arbitrary time
setTimeout(store.stop, 5000)
This is of course far away from elegant; I've been trying to wrap my head around generators (which sincerely I haven't really used before) in order to improve this kind of code.
The syntax can be improved a bit if we use async generators; instead of passing up the promise returned by the iterable to flow, one just await it for to resolve. But as far as I have played around, there's not much more to simplify.
The mst-example-boxes is quite simple because of the use of applyAction, but I don't believe that's how most applications in the wild would use websocket to update the store.
I would love to help craft a diff kind of websocket mst example that involves this kind of subscription problem - once I find a nicer way. :smile:
cc: @mweststrate
What about this?
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const Model = types
.model({
value: types.number
})
.actions(self => {
let stopUpdating = false;
return {
afterCreate: flow(function*() {
while (true && !stopUpdating) {
yield sleep(1000);
self.value = self.value + 1;
}
}),
stop() { // exposing action :(, that doesn't change model.
stopUpdating = true;
}
}
});
@effrenus Yea, it's another way to go about this, but personally, as I said, I would avoid such autostart mechanics in afterCreate. Besides, if you are already exposing the stop, why not expose start as well? :) Btw, I think it's perfectly fine to expose action that modifies volatile internal state only. It doesn't really matter in the end.
And thank you for showing how to wrap timer into the promise and then into flow. I should have probably shown an example instead of trying to explain it with words :) @dfilatov Is it more clear now?
Also, @stefanmaric shame on your for trying to hijack a thread with your own unrelated issue. I haven't even tried to investigate that code, it's too bulky. Please, make your own issue if you still need help with it and try to minimize the code to point out an issue at hand.
Nah, I take it back, it's a bad example with the while loop, it's kinda hard to reason about that. Here is a bit more robust solution I am using in my project. It's very useful when I need many parts of the UI to be updated in time while using a single timer. Notice there is no need for flow or promises. You can also supply your own time resolver function which helps with tests.
https://gist.github.com/FredyC/fa2d1e066cf01a942342877e5768dd87
You can do cool stuff like this. It's bit simplified here, you can see working example here: https://codesandbox.io/s/jvxwo9xw8y
const Countdown = ({ endTime, timingModel }) => (
<Observer render={() => (
<div>{`Remaining: ${getSecond(endTime - timingModel.bySecond)}s`}</div>
)} />
)
Also, @stefanmaric shame on your for trying to hijack a thread with your own unrelated issue. I haven't even tried to investigate that code, it's too bulky. Please, make your own issue if you still need help with it and try to minimize the code to point out an issue at hand.
@FredyC I invite you to read it; I'm sure it won't take you long; it is not my specific problem, but a solution to the counter problem that initiated this thread - it is just a counter, really.
The workaround proposed by @effrenus and the one of yours are neat for this specific counter example; but there are lots of cases that involve the store subscribing to sources of updates. Call it setInterval, polling, observable, WebSocket, firebase, etc. and the idea behind hiding actions from the consumer is to make the store responsible for subscribing and updating accordingly to these updates, moving away the logic from the view.
It feels like you are trying to elevate MST to something it isn't. It's the state management. I don't think it should be also covering business logic. Extract that somewhere else, update the state from the outside with action and observe changes. In my opinion, it's much simpler, understandable, maintainable and testable like that.
the idea behind hiding actions from the consumer is to make the store responsible for subscribing and updating accordingly to these updates, moving away the logic from the view.
I can understand that, but how is exposing an action an issue here? Sure, it's not pretty for a consumer, but that can be surely mitigated by some convention or documentation. I would say it's not worth the effort trying to hide something and biting yourself in the leg on the way. Besides, I don't see it as an issue if a consumer can actually modify public state by public action. It's their state, why they shouldn't be able to modify it?
Extract that somewhere else, update the state from the outside with action and observe changes. In my opinion, it's much simpler, understandable, maintainable and testable like that.
how is exposing an action an issue here? Sure, it's not pretty for a consumer, but that can be surely mitigated by some convention or documentation.
I would say it is about constrains; I think that's the whole purpose of using mobx-state-tree over mobx.
And if we follow that path, why even support async actions? - Do all your async logic outside of MST and simply update the store with a public action once you're done.
These tools exist to satisfy a need; MST async actions exists for the same reason readux-saga exists: devs want it.
And I think that's the topic in this thread: the need to handle certain async side-effects in a fashionable way. It is nice to discuss alternatives but we shouldn't discard the original point just because one can achieve something similar by omitting a requirement (to be hidden from the consumer).
The snippet I shared is the best I could get to answer "How to asynchronously update property inside actions without exposing action?" with setInterval as use case; @effrenus' one is indeed way better but leaves other similar use cases without answer.
I guess there is a huge gap if you are using MST to create some lib. I could understand that want to keep something private there to avoid handling if consumer misuses that. However, for an actual app, I don't see a purpose in such constraints as any developer can change that to suit his needs.
Perhaps I am using MST in a wrong way, but I am more than satisfied with it. It's much easier for me to write and test async code separately without depending on MST. I guess this pattern came with Redux to "rape" state management with business logic. Personally, I don't like it so I cannot help here I guess.
Btw, MobX itself allows you to hide private stuff just fine (eg. closures, WeakMaps...). The MST is forcing you to expose it so you've got that kinda reversed ;)
I'm just finding this after "hijacking" another issue where expressed a similar confusion to that of @stefanmaric. Just to add my own curiosity to this issue for posterity:
I'm sorry for asking this here, but couldn't find a straight answer elsewhere:
If we'd like to have middleware that can subscribe to some external data source (e.g., a streaming API/websocket or @live graphql endpoint (TBD)), how might we do that? I.e., is this kind of observable data injection something that should be handled in the actions or in some middleware and - in either case - how would you recommend it be accomplished?
to defuse this discussion a bit - flow as well as async iterators made their way into mobx itself and will find their way into MST too. the plan is to expand the mobx flow to also be able to use it within MST.
with such the initial problem the OP asked could be solved using an async generator.
e.g.:
function delay(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms)
})
}
async function* asyncInterval(ms) {
while (true) {
yield await delay(ms);
}
}
const Store = types.model({
count: types.number
}).actions(self => ({
afterCreate: flow(async function * () {
for await (const _ of asyncInterval(1000)) {
self.count++;
}
})
}))
For now: the ways proposed above (second action / sleep) work until we have async iterators.
closing in favour of https://github.com/mobxjs/mobx-state-tree/issues/720
Most helpful comment
to defuse this discussion a bit - flow as well as async iterators made their way into mobx itself and will find their way into MST too. the plan is to expand the mobx flow to also be able to use it within MST.
with such the initial problem the OP asked could be solved using an async generator.
e.g.:
For now: the ways proposed above (second action / sleep) work until we have async iterators.