menu

Statecharts

The Statecharts community on Spectrum is (along with spectrum) MOVING TO OTHER PLATFORMS: For statecharts discussions in general go to Statecharts Discussion on GitHub (link) or Gitter (link). For XState-specific questions, go to the XState discussion forum for Q&A or the Stately discord chat to chat.

Channels
Team

Why should the context be updated with assign

February 11, 2019 at 5:47pm

Why should the context be updated with assign

February 11, 2019 at 5:47pm
I was reading the discussion at https://github.com/mobxjs/mobx-state-tree/issues/1149, and I don't quite understand why the context should be modified using assign, and why assign should receive an immutable object.
If I simply modify a property of the context directly, like in this codesandbox (https://github.com/mobxjs/mobx-state-tree/issues/1149), there doesn't seem to be a downside.
What am I not getting?

February 11, 2019 at 6:16pm
SCXML compatibility and static analysis. Also, it should be clear that mutation leads to hard-to-track bugs and loss of datamodel change tracking. This is XState/MST specific though - can we keep it in that GitHub thread instead of on Spectrum? 🙏Thanks!
  • reply
  • like
Sorry for the confusion, my question actually doesn't relate to MST at all, I pasted in the wrong codesandbox link... here is the correct one (no MST, pure xstate).
But I guess that you answered my question already, so thanks !
Edited
  • reply
  • like
⚠️ One important reason that you don't want to mutate context is that state machines are rarely in isolation - there may be many instances of a machine, and many machines talking to each other (in which we definitely don't want a machine mutating some other machine's state - in many cases, it's even impossible).
For instance, if you have two instances of a counter machine:
const s1 = interpret(counterMachine).start();
const s2 = interpret(counterMachine).start();
s1.send('INC');
If the action tied to INC on a certain state in counterMachine mutates counterMachine.context, then s2 will unintentionally also have that mutated context.
In short, avoid mutation. Think event-based and functional (statecharts are for reactive systems).
  • reply
  • like

February 13, 2019 at 9:40pm
I would prefer immutable data structures, because it forces the changes the data structure to happen through events, allowing the statecharts to re-evaluate any guards. Remember that statecharts are inherently event driven and reactive in nature. If you were allowed to make an arbitrary change of the data, then the statechart might not be in the right state. There are things like automatic, guarded transitions that transition as soon as a condition holds true — and those guards are only evaluated when processing an event.
Edited
  • reply
  • like

February 14, 2019 at 11:17am
You mention a good point. It doesn't quite apply to my example I think, since I was mutating the context within an action, not outside of the statechart, so no guards could be skipped. However, your comment made me think it might be quite useful to have a way to enforce an immutable context. I tried to do that by initializing and updating the context using immer, which prevents you to modify the generated object directly. However, this doesn't work, since xstate's assign uses Object.assign internally to update the context. The resulting context is then mutable, and there doesn't seem to be a way to change this.
  • reply
  • like

February 15, 2019 at 7:50pm
Updating the context in XState is still immutable:
Object.assign({}, acc, partialUpdate);
Nothing is being mutated.
  • reply
  • like

February 17, 2019 at 9:04pm
You are right of course, I wasn't clear with my statement. I meant rather that it would be useful (for me at least) to freeze the context in development, so that it is not possible to mutate it without using assign (as I foolishly did in my first example). Somewhat like the auto freezing that immer does when producing a new state tree with produce.
  • reply
  • like

February 18, 2019 at 5:08pm
You can use immer
assign(immer((ctx, e) => { ctx.foo = e.bar }))
Edited
  • reply
  • like
Yes, that is what I tried to do, but it doesn't freeze the context, because the context is not replaced by the object returned by immer. That is because the context is updated using Object.assign (as shown in 's post above). I'll just have to be careful to avoid mutating the context directly.
  • reply
  • like
Post an issue: github.com/davidkpiano/xstate/issues/new - that would make a good enhancement, to control how assign works and customize it.
  • reply
  • like