โ Question
How do you decide the how fine-grained your actors are? At first glance I'd like to just implement everything in XState (especially with the new Typestates feature) and get rid of anything else (even js/ts functions?) as much as possible, but I am not sure about the downsides and I just can't see "conceptual the barrier". Can you please elaborate on the downsides? What to put and what not to put in XState? It feels like a new language on top of ts, which is totally cool because of the ecosystem, but it is just not clear to me what's really happening. What is really happening? ๐ Will that lead to a "dependently typed-ish" way?
Putting everything into actors has the same upsides and downsides of having local, private state instead of shared state. Of course, shared state makes things easier, but it also makes it easier to introduce bugs or impossible states, race conditions, etc.
Actors are also distributed by nature, so e.g., you don't get clear "time-travel" abilities like in Redux, but that isn't even feasible in Redux itself because your entire application is distributed, no matter how much state you put into a single store.
@davidkpiano can you explain in more detail why the "time-travel" feature is not feasible in Redux? Thx.
can you explain in more detail why the "time-travel" feature is not feasible in Redux?
Absolutely. Suppose you have a Redux reducer where an event changes the state such that isLoading: true (or something like that... hopefully the developer has better sense than to use boolean flags).
Now suppose that at the same time, some side-effect happened (to load data). The first problem is: did the side-effect happen _as a result_ of the state change (i.e., as a result of isLoading going from false to true), or did it happen before, and the state change occur afterwards? If you're using redux-thunk, it is the latter. You start the promise, and at the same time, send some { type: 'LOADING' } event.
But that's not the main problem. In this "loading" state, that side-effect promise is being executed. How do you model that, or keep track of that?
Let's say you eventually reach some "success" state (isLoading: false and data: {...}) and you want to time-travel back to that loading state.
What should happen?
Long story short, Redux time travel only works for side-effect-free code.
Putting everything into actors has the same upsides and downsides of having local, private state instead of shared state. Of course, shared state makes things easier, but it also makes it easier to introduce bugs or impossible states, race conditions, etc.
Thanks, that helps clarifying.
However, related questions (in my head):
How do you decide something to implement via explicit state machines vs implicit state machines? Aka: why Xstate is not rewritten in Xstate? :)
My concern is that if I implement anything implicitly it can be OK for now but when requirements and code changes it is risky to maintain that implicit code and I'd rather rewrite it explicitly. But knowing that can happen at any time I don't want to waste resources writing implicit code. However I am not sure about the tradeoff: can you elaborate on that part? How do you decide if you want to go with implicit state? I'm quite sure there are some "red flags" in patterns you already recognize early which makes you skip the implicit code writing-deletion part. Sure, sometimes it does worth it to go with implicit, but when?
It's always easier to start with implicit, but it's always too late to change to explicit.
The thing I want to eliminate is rewriting the same code over and over again.
Also: is there any hope for a something like a codemod which takes an implicit code and puts out an (almost but not totally perfect) explicit code or at least a skeleton?
How do you decide something to implement via explicit state machines vs implicit state machines?
Here's the pattern I always follow:
Over time, and with enough practice, you will immediately recognize logic as a state machine/statechart and immediately start with that instead of an implicit state machine. For example, if I'm tasked with modeling an async flow that is cancellable, I know how difficult that is with implicit state machines, so I'll model it as a statechart instead.
Thanks a lot!
What do you use for pubsub? Rx?
It's interesting pen and paper is good for you, for me it did not scale well lately, I'd rather use plantuml. What is the trick? :) I'd be glad to see a scratch :) but I don't want to steal too much time from you. :)
I would love to see some of your pen and paper sketches @davidkpiano ๐
What should happen?
- Should the side-effect be re-executed?
- Should we assume that the side-effect is already executed?
- Is there even a straightforward way to know _which_ side-effects were executed? (short answer: no)
Can't it be answered? We can assume that middlewares are interceptor responders to dispatched events, so in case we have a LOAD_STARTED and LOAD_SUCCESS event, and we go back to LOAD_STARTED, then the time travel engine sets the store to the snapshot before LOAD_STARTED, dispatches LOAD_STARTED, and then the middleware re-runs the fetch.
What's the catch in this?
That doesn't fit every use-case. If some fetch already ran, you might not want to run it again.
Added to Roadmap for improved documentation
Most helpful comment
I would love to see some of your pen and paper sketches @davidkpiano ๐