Closed dakom closed 5 years ago
I think I got hierarchy wrong - hierarchy is not about multiple machines, but rather grouping states within one machine (see #322)
For hierarchical and invoke, that's correct, for the most part. However...
This isn't really part of the API, but it's notable that an interpreter can be created, and then referenced directly from another machine ...
⚠️ This is not recommended, because there is no deterministic way to statically interpret/model that, and it makes it much more difficult to visualize/analyze. Always use send(...)
or sendParent(...)
to communicate between statecharts.
Thanks... but isn't manual references how you communicated in the Todo MVC ?
For example these handlers which get called by the TodoMachine actions:
onChange={todo => send({ type: "TODO.COMMIT", todo })}
onDelete={id => send({ type: "TODO.DELETE", id })}
Ah, I mean that you shouldn't reference other services (interpreted machines) from a machine:
// ⚠️ DO NOT DO THIS ⚠️
{
actions: () => {
// ❌ do not reference other services directly in actions!
fooService.send('SOME_EVENT');
}
}
Instead, you should use send
or sendParent
:
{
invoke: {
id: 'foo',
src: // ...
},
on: {
TRIGGER: {
actions: send('SOME_EVENT', { to: 'foo' })
}
}
}
But in your Todo MVC example, you are calling service.send
in the action, no?
In other words, the send
being passed in the onChange/onDelete
handlers above is not actions.send
but rather fooService.send
I don't see a way to link to specific lines on codesandbox so I'll include the relevant snippets (and cut out the surrounding parts), hope this makes sense!
useMachine.js
return [current, service.send];
Todos.jsx
const [state, send] = useMachine(persistedTodosMachine);
<Todo
onChange={todo => send({ type: "TODO.COMMIT", todo })}
/>
Todo.jsx
todoMachine.withConfig(
{
actions: {
notifyChanged(ctx) {
onChange({});
}
}
},
service.send(...)
is never called in the actions, nor in the machines. It is only called by the UI:
<Todo
onChange={todo => send({ type: "TODO.COMMIT", todo })}
/>
actions: {
notifyChanged(ctx) {
onChange({...}); //<-- is this not calling service.send from the actions?
}
}
Maybe, maybe not. You can easily have this:
<Todo
key={todo.id}
todo={todo}
onChange={todo => {
someOtherWayToCommitTodo(todo);
}}
onDelete={id => {
someOtherWayToDeleteTodo(id);
}}
/>
That is, both the TodoMachine
and the <Todo>
component are completely agnostic as to the implementation details. They're just responsible for calling onChange
and onDelete
- they don't know that they're sending an event to a statechart owned by the parent <Todos>
.
ahhh okay... so you're talking more like in terms of design goals, not in terms of bugs that could be created due to holding strong references or something?
i.e. it's technically fine to call service.send
from an action, but when doing so one should be aware of the strong coupling and how it could make things messy to detangle later?
I wouldn't go as far as to say it's "technically fine" - it should be avoided. Decoupling is a primary concern of statecharts.
Gotcha, thanks again!!
Hi, I am having a similar issue. I'm attempting to formalize a game with a dynamic and undetermined number of players with xstate. However, since I cannot invoke from an action, and therefore create additional submachines/services on demand, this isn't possible. And then I must regress to using a strategy like the one in the Todo example. Ideally there would be notation in statecharts for the dynamical creation of parallel states or invoking in an action.
...dynamic and undetermined number of players
Using context
would be the best fit here. This might be better discussed in the Spectrum statecharts community.
Alright. I have opened a thread about this; the tldr is that the state chart formalism cannot yet represent a dynamical list of state machines.
It seems that there are 3 ways to organize different machines for communication between them:
Is the following correct?
Hierarchical: Helps to keep a machine tidy by taking a bunch of state that can be clustered, and making it into one cluster. These sub-machines are really part of the parent machine and could have been written at the top level; hierarchy is simply a technique to make "state explosion" much more manageable (and it becomes a huge difference considering how the API provides this, e.g. sub-state matching and transitions)
invoke(): Launches something like a temporary machine. The Promise example in the docs is a good concept here - you wouldn't want to have one Promise for an app, often you want to create new Promises and respond to them as they finish. Invoke allows creating and then responding to sub-machines created on the fly
Manual reference: This isn't really part of the API, but it's notable that an interpreter can be created, and then referenced directly from another machine, for example in its
actions
. This allows for truly separating and communicating between different components in an app.