Have noticed that forbidden transitions in strict mode are not accepted. The machine throws a strict mode error.
As per documentation transitions defined as undefined or {actions: []} are meant to be recognized as valid transitions. When a matching event is sent the machine throws an error.
Example:
import { Machine } from 'xstate';
const testMachine = Machine({
strict: true,
initial: 'someState',
states: {
someState: {
on: {
FORBIDDEN_UNDEFINED: undefined,
FORBIDDEN_EMPTY_ACTIONS: { actions: []},
FORBIDDEN_OK: 'someState',
}
},
}
})
let currentState = testMachine.initialState
function send(event) {
currentState = testMachine.transition(currentState, event)
console.log(currentState.value)
}
send('FORBIDDEN_OK') // someState
send('FORBIDDEN_UNDEFINED') // Machine '(machine)' does not accept event 'FORBIDDEN_UNDEFINED'
send('FORBIDDEN_EMPTY_ACTIONS') // Machine '(machine)' does not accept event 'FORBIDDEN_EMPTY_ACTIONS'
Have been broken since 4.0.0-11
Thanks for the report - I will take a look today at this, should be an easy fix. That said - could you describe your use case for strict mode? We are considering removing it in the future.
Thanks for the prompt reply.
I don't intend to retain the strict mode in production, just for testing. In my case, a third party library emits a sequence of events, some undocumented. So I would like to catch all & fail-fast if some events are become unaccounted for in the machine. So outside testing I don't have a valid case for the strict mode.
Hope this helps
Irek
I've identified the problem, I'm just unsure if we should fix this right now. I need to think about it more and discuss it with David.
When we drop strict mode from the core in the next major, our recommendation for a replacement will be just using a wildcard descriptor on the root - you could try this strategy already.
Machine({
on: {
'*': { /* your logic of failing */ }
}
})
Thanks for the tip, works fine and seems to do exactly the same as the strict mode.
Ok, the actual workaround to simulate a forbidden transition is to simply specify the self-transition explicitly, i.e. 'SOME_EVENT': 'sameState', and that's fine.
If you refer to using wildcard event transition instead of strict mode it has produced some unexpected behaviour.
The wildcard action '*': { /* your logic of failing */ } placed at the root level appears to get triggered by an event for which there is, in fact, a state transition with false condition. Additionally, self-transitions that are internal by default become external self-transitions that trigger service on re-entry. I didn't dig into it any further but, perhaps, there is more to it. Is this something that can be explained?
Machine({
...
on: {
'*': { /* logic of failing */ }
},
someState: {
invoke: {
id: 'serviceId',
src: 'someService'
},
on: {
EVENT_FALSE: { actions: 'someAction', cond: false },
EVENT_EXTERNAL_TRUE: { actions: 'someAction' },
EVENT_INTERNAL_TRUE: { actions: 'someAction', internal: true }
}
}
})
...
send('EVENT_FALSE') // triggers logic of failing
send('EVENT_EXTERNAL_TRUE') // doesn't trigger logic of failing, however, triggers service on self-transition?
send('EVENT_INTERNAL_TRUE') // doesn't trigger any of the above
This is expected. The guarded transition for EVENT_FALSE needs some condition (please use cond: () => false) to be satisfied. If it is not, then no transition matches, and it's as if there is no transition for that "falsy" event, so it falls back to '*'.
However, you can be explicit:
EVENT_FALSE: [
{ actions: 'someAction', cond: () => false },
{ target: undefined }
]
Thanks for the explanation!